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=500Min/etc/systemd/journald.conf, orjournalctl --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.