Without log rotation, log files grow forever. A busy web server generates gigabytes of access logs. A verbose application can fill a filesystem in hours. logrotate is the standard Linux tool that automatically renames, compresses, and eventually deletes old log files on a schedule — keeping your filesystem healthy while preserving enough history to be useful.
The configuration syntax looks simple but has enough edge cases to trip people up — especially around postrotate scripts, the copytruncate option, and date-based naming. This page covers it thoroughly including testing and troubleshooting.
logrotate runs daily via cron (/etc/cron.daily/logrotate) or
a systemd timer. On each run it reads its configuration, checks each defined
log file, and rotates those whose rotation trigger has been met.
app.log:
app.log → renamed to app.log.1app.log is created (or the app reopens it)app.log.1 → compressed to app.log.1.gzapp.log.2.gz → app.log.3.gz (shifted)rotate N count are deleted| Location | Purpose |
|---|---|
| /etc/logrotate.conf | Global defaults and includes |
| /etc/logrotate.d/ | Per-application drop-in configs (preferred) |
| /var/lib/logrotate/logrotate.status | State file — when each file was last rotated |
# View the global defaults cat /etc/logrotate.conf # View a package-installed config cat /etc/logrotate.d/nginx cat /etc/logrotate.d/syslogAnnotated /etc/logrotate.conf:
/etc/logrotate.conf can be overridden in individual
drop-in files in /etc/logrotate.d/. The drop-in
always wins over the global default.
Create /etc/logrotate.d/myapp for your application:
# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0644 myapp myapp
sharedscripts
postrotate
systemctl reload myapp 2>/dev/null || true
endscript
}
daily — rotate every dayrotate 14 — keep 14 rotated files before deletingcompress — gzip rotated filesdelaycompress — compress on the NEXT rotation, not this one (see Example 4)missingok — don't error if log file is missingnotifempty — don't rotate if file is emptycreate 0644 myapp myapp — create new file with these permissions/ownersharedscripts — run postrotate once even if multiple files matchedpostrotate...endscript — run after rotation (reload app)# Time-based rotation (most common) /var/log/myapp/app.log { daily # rotate every day weekly # or every week monthly # or every month rotate 30 # keep 30 rotated files } # Size-based rotation -- rotate when file exceeds this size /var/log/myapp/app.log { size 100M # rotate when file reaches 100MB rotate 10 compress missingok } # Size with minimum time -- rotate when BOTH conditions are met /var/log/myapp/app.log { daily size 50M # but only if also 50MB or larger rotate 7 compress } # Date-stamped filenames instead of numbered /var/log/myapp/app.log { daily rotate 30 dateext # app.log-20260425 instead of app.log.1 dateformat -%Y%m%d # format for the date suffix compress }
app.log-20260425.gz) are much
easier to work with than numbered files (app.log.3.gz).
You instantly know when each file covers without counting backwards.
delaycompress is one of the most commonly needed but
least understood logrotate options. It prevents the JUST-rotated file
from being compressed immediately:
# Without delaycompress: # Day 1: app.log rotated to app.log.1, immediately compressed to app.log.1.gz # Problem: if the app still has app.log.1 open, it writes to the compressed file # which corrupts it # With delaycompress: # Day 1: app.log rotated to app.log.1 (uncompressed) # App can still write to app.log.1 via its open file handle # Day 2: app.log.1 compressed to app.log.1.gz (app has closed the handle by now) # app.log rotated to app.log.1 (uncompressed, for same reason) /var/log/myapp/app.log { daily rotate 7 compress delaycompress # ALWAYS pair with compress missingok notifempty sharedscripts postrotate kill -USR1 $(cat /var/run/myapp.pid 2>/dev/null) 2>/dev/null || true endscript }
delaycompress together with a postrotate signal to
make the app reopen its log file.
Most daemons need to be told that their log file has been rotated so
they open the new file. The postrotate script does this:
# Reload nginx after log rotation (nginx reopens logs on HUP) /var/log/nginx/*.log { daily rotate 52 compress delaycompress missingok notifempty sharedscripts postrotate nginx -s reopen 2>/dev/null || true endscript } # Reload Apache /var/log/httpd/*log { daily rotate 52 compress delaycompress missingok sharedscripts postrotate /bin/kill -USR1 $(cat /run/httpd/httpd.pid 2>/dev/null) 2>/dev/null || true endscript } # Reload rsyslog /var/log/messages /var/log/secure /var/log/cron { weekly rotate 26 compress delaycompress sharedscripts postrotate /usr/bin/systemctl kill -s HUP rsyslog.service 2>/dev/null || true endscript } # copytruncate -- alternative when you cannot signal the app # Copies the log then truncates the original in place # App keeps writing to same inode -- no signal needed /var/log/myapp/app.log { daily rotate 7 compress copytruncate # use only when postrotate is not possible }
copytruncate
copies the log and then empties the original file in place —
the app keeps writing to the same file descriptor, no signal needed.
The downside is a small window where log entries between the copy
and the truncate are lost. Use postrotate whenever
possible; use copytruncate when you cannot signal the app.
# Dry run -- show what WOULD happen without doing it sudo logrotate -d /etc/logrotate.d/myapp sudo logrotate -d /etc/logrotate.conf # Force rotation NOW regardless of schedule sudo logrotate -f /etc/logrotate.d/myapp sudo logrotate -f /etc/logrotate.conf # force ALL logs # Verbose output -- see exactly what logrotate is doing sudo logrotate -v /etc/logrotate.d/myapp # Combine: force + verbose (best for troubleshooting) sudo logrotate -vf /etc/logrotate.d/myapp # Debug mode (very verbose) sudo logrotate -d -v /etc/logrotate.d/myapplogrotate -v output:
# Check when each log was last rotated sudo cat /var/lib/logrotate/logrotate.status sudo cat /var/lib/logrotate.status 2>/dev/null # older RHEL path # logrotate not running? Check the cron job ls -la /etc/cron.daily/logrotate cat /etc/cron.daily/logrotate # Check systemd timer (newer systems) systemctl status logrotate.timer systemctl list-timers logrotate* # Log file not rotating -- common causes: # 1. Config syntax error sudo logrotate -d /etc/logrotate.d/myapp 2>&1 | grep -i error # 2. Not enough time has passed (check status file) grep myapp /var/lib/logrotate/logrotate.status # 3. File is empty (notifempty directive) ls -la /var/log/myapp/app.log # 4. Wrong permissions on config file (must not be world-writable) ls -la /etc/logrotate.d/myapp sudo chmod 644 /etc/logrotate.d/myapp # 5. postrotate script failing sudo logrotate -vf /etc/logrotate.d/myapp 2>&1 | tail -20 # Reset the status for a specific log (force fresh rotation) sudo sed -i '/myapp/d' /var/lib/logrotate/logrotate.status
/etc/logrotate.d/ has a syntax error,
logrotate skips it without warning — the log just never rotates
and grows until the disk fills. Always test with logrotate -d
after creating or editing a config file.
# /etc/logrotate.d/webapp -- production web application /var/log/webapp/access.log /var/log/webapp/error.log /var/log/webapp/app.log { daily rotate 90 compress delaycompress missingok notifempty dateext dateformat -%Y%m%d create 0640 webapp webapp sharedscripts prerotate if [ -d /etc/logrotate.d/httpd-prerotate ]; then \ run-parts /etc/logrotate.d/httpd-prerotate; \ fi endscript postrotate systemctl reload webapp 2>/dev/null || true logger -t logrotate "webapp logs rotated" endscript } # /etc/logrotate.d/database-backup -- backup job logs /var/log/db-backup.log { weekly rotate 52 compress delaycompress missingok notifempty size 10M create 0640 root root } # /etc/logrotate.d/custom-app -- app that cannot be signaled /var/log/legacy-app/output.log { daily rotate 30 compress copytruncate missingok notifempty dateext }
| Directive | Meaning |
|---|---|
| daily / weekly / monthly | Rotation frequency |
| rotate N | Keep N rotated files before deleting |
| size N[kMG] | Rotate when file exceeds this size |
| compress | Gzip rotated files |
| delaycompress | Compress on next rotation, not this one |
| nocompress | Do not compress |
| missingok | Do not error if log file is missing |
| notifempty | Do not rotate if file is empty |
| ifempty | Rotate even if file is empty (default) |
| create MODE USER GROUP | Create new log file with these attributes |
| nocreate | Do not create a new empty file after rotation |
| copytruncate | Copy then truncate in place (no signal needed) |
| dateext | Use date suffix instead of number |
| dateformat FORMAT | Date format for dateext (e.g., -%Y%m%d) |
| sharedscripts | Run pre/postrotate once for all matched files |
| postrotate...endscript | Run script after rotation |
| prerotate...endscript | Run script before rotation |
| firstaction...endscript | Run before any rotation in this block |
| lastaction...endscript | Run after all rotations in this block |
| Command | What it does |
|---|---|
| sudo logrotate -d /etc/logrotate.d/myapp | Dry run — show what would happen |
| sudo logrotate -v /etc/logrotate.d/myapp | Verbose run |
| sudo logrotate -f /etc/logrotate.d/myapp | Force rotation now |
| sudo logrotate -vf /etc/logrotate.d/myapp | Force with verbose output |
| sudo cat /var/lib/logrotate/logrotate.status | See when each log was last rotated |
| ls /etc/logrotate.d/ | List all drop-in configs |
| systemctl status logrotate.timer | Check systemd timer status |