Logs grow forever unless something trims them. On a small VPS, an unrotated access log or app log can fill the disk in weeks. logrotate renames, compresses and deletes old logs on a schedule, and it’s already installed and running on virtually every Linux distribution.

How it runs

logrotate isn’t a daemon. It runs daily from a systemd timer (logrotate.timer) or /etc/cron.daily/logrotate, reads its configuration and rotates whatever is due.

  • Main config: /etc/logrotate.conf
  • Per-application configs: /etc/logrotate.d/ (nginx, apache2, mysql-server, rsyslog…)
  • State file (when each log was last rotated): /var/lib/logrotate/status
systemctl list-timers logrotate.timer

A config for your application

# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 myapp adm
    sharedscripts
    postrotate
        systemctl kill -s HUP myapp.service >/dev/null 2>&1 || true
    endscript
}

What each line does:

Directive Meaning
daily / weekly / monthly How often to rotate
size 100M Rotate when bigger than 100 MB (ignores time)
maxsize 100M Rotate on schedule or when bigger than this
rotate 14 Keep 14 old files, delete older ones
compress gzip old logs
delaycompress Keep the most recent old log uncompressed (useful if the app still writes to it briefly)
missingok No error if the log doesn’t exist
notifempty Skip empty logs
create 0640 user group Create a new empty log with these permissions
dateext Name old logs app.log-20261014 instead of app.log.1
su user group Rotate as this user (needed when the log directory isn’t root-owned)

Telling the app about the new file

After the log is renamed, the application still has the old file open and keeps writing into app.log.1. There are two solutions.

1. Signal the app to reopen its logs (preferred). Use postrotate:

postrotate
    [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript

Nginx reopens logs on USR1. Many daemons use HUP. sharedscripts runs the script once for all matched files instead of once per file.

2. copytruncate. For apps that can’t reopen logs:

copytruncate

logrotate copies the log, then truncates the original in place, so the app keeps writing to the same file. The downside is that lines written between the copy and the truncate can be lost. It’s fine for most app logs.

Test before relying on it

sudo logrotate -d /etc/logrotate.d/myapp     # dry run: shows what would happen
sudo logrotate -f /etc/logrotate.d/myapp     # force a rotation now
sudo logrotate -v /etc/logrotate.conf        # verbose run of everything

Check the state file to see when a log was last rotated:

grep myapp /var/lib/logrotate/status

Common problems

“error: skipping … because parent directory has insecure permissions” — the log directory is writable by a non-root user or group. Add su myapp myapp to the config block.

Log keeps growing despite rotation — the app still writes to the rotated file. Add a postrotate signal or copytruncate. sudo lsof +L1 shows deleted files still held open. See lsof.

Rotation happens only once a day even with size — logrotate itself runs daily. For fast-growing logs, run it hourly: copy the timer with OnCalendar=hourly, or add an hourly cron entry for that config. Combine it with maxsize.

Config ignored — the files in /etc/logrotate.d/ must be valid. One syntax error can abort processing. Run logrotate -d /etc/logrotate.conf and read the errors.

Disk filled up anyway — logrotate deletes only files matching its patterns. Old *.gz files from a changed config, or logs in other directories, need cleanup separately. See disk full troubleshooting.

Docker and journald logs

  • Docker container logs aren’t managed by logrotate. Set limits in /etc/docker/daemon.json ("log-opts": {"max-size": "10m", "max-file": "3"}). See docker logs.
  • journald has its own limits: SystemMaxUse=500M in /etc/systemd/journald.conf, or journalctl --vacuum-size=500M. See journalctl.

A typical Nginx config

Debian and Ubuntu ship this in /etc/logrotate.d/nginx:

/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    prerotate
        if [ -d /etc/logrotate.d/httpd-prerotate ]; then run-parts /etc/logrotate.d/httpd-prerotate; fi
    endscript
    postrotate
        invoke-rc.d nginx rotate >/dev/null 2>&1
    endscript
}

Add per-site logs elsewhere (/var/www/site/logs/*.log) as their own block.

Quick reference

Task Command / directive
Dry run logrotate -d /etc/logrotate.d/x
Force now logrotate -f /etc/logrotate.d/x
Keep N rotate N
By size size 100M / maxsize 100M
Compress compress + delaycompress
Reopen logs postrotate … endscript
App can’t reopen copytruncate
Non-root log dir su user group

Related: Nginx logs and rsyslog.

  • Linux
  • Logs
  • Configuration