🔄 logrotate

Logging & Log Management Series: Part 1 — Basics  |  Part 2 — journald  |  Part 3 — logrotate  |  Part 4 — Centralized Logging

Why logrotate Matters

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.

How logrotate Works

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.

The rotation sequence for a file named app.log:
  1. app.log → renamed to app.log.1
  2. A new empty app.log is created (or the app reopens it)
  3. Old app.log.1 → compressed to app.log.1.gz
  4. app.log.2.gzapp.log.3.gz (shifted)
  5. Files beyond the rotate N count are deleted

Configuration File Locations

LocationPurpose
/etc/logrotate.confGlobal defaults and includes
/etc/logrotate.d/Per-application drop-in configs (preferred)
/var/lib/logrotate/logrotate.statusState file — when each file was last rotated

Examples

1
Reading /etc/logrotate.conf
# View the global defaults
cat /etc/logrotate.conf

# View a package-installed config
cat /etc/logrotate.d/nginx
cat /etc/logrotate.d/syslog
Annotated /etc/logrotate.conf:
# Rotate weekly by default weekly # Keep 4 weeks of logs rotate 4 # Create new empty log file after rotation create # Compress rotated logs compress # Include all drop-in configs include /etc/logrotate.d # System-specific logs /var/log/wtmp { monthly create 0664 root utmp rotate 1 }
Global settings are defaults only. Any setting in /etc/logrotate.conf can be overridden in individual drop-in files in /etc/logrotate.d/. The drop-in always wins over the global default.
2
Writing a logrotate Drop-in Config

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
}
Breaking down the directives:
3
Size-Based vs Time-Based Rotation
# 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
}
💡 Use dateext for busy servers. Date-stamped filenames (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.
4
delaycompress — The Most Misunderstood Option

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
}
⚠️ Use delaycompress with any app that does not reopen its log file after rotation. If your application keeps the log file open continuously (many daemons do), the just-rotated file still has an open file handle. Compressing it immediately causes problems. Use delaycompress together with a postrotate signal to make the app reopen its log file.
5
postrotate Scripts — Signaling Applications

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 vs postrotate: 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.
6
Testing logrotate — Dry Run and Force
# 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/myapp
logrotate -v output:
reading config file /etc/logrotate.d/myapp Handling 1 logs rotating pattern: /var/log/myapp/*.log after 1 days (14 rotations) empty log files are not rotated, old logs are removed switching euid to 0 and egid to 0 considering log /var/log/myapp/app.log log needs rotating rotating log /var/log/myapp/app.log, log->rotateCount is 14 dateext suffix '-20260425' glob pattern '-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' renaming /var/log/myapp/app.log to /var/log/myapp/app.log-20260425 creating new /var/log/myapp/app.log mode = 0644 uid = 1001 gid = 1001 running postrotate script compressed /var/log/myapp/app.log-20260424.gz
💡 Always test with -d first. The dry run mode shows you exactly what logrotate will do without touching any files. Run it after writing any new config to catch syntax errors and confirm the behavior before the real run.
7
Troubleshooting logrotate
# 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
⚠️ logrotate silently skips bad configs. If a drop-in file in /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.
8
Complete Real-World Examples
# /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
}

Quick Reference

logrotate Directive Reference

DirectiveMeaning
daily / weekly / monthlyRotation frequency
rotate NKeep N rotated files before deleting
size N[kMG]Rotate when file exceeds this size
compressGzip rotated files
delaycompressCompress on next rotation, not this one
nocompressDo not compress
missingokDo not error if log file is missing
notifemptyDo not rotate if file is empty
ifemptyRotate even if file is empty (default)
create MODE USER GROUPCreate new log file with these attributes
nocreateDo not create a new empty file after rotation
copytruncateCopy then truncate in place (no signal needed)
dateextUse date suffix instead of number
dateformat FORMATDate format for dateext (e.g., -%Y%m%d)
sharedscriptsRun pre/postrotate once for all matched files
postrotate...endscriptRun script after rotation
prerotate...endscriptRun script before rotation
firstaction...endscriptRun before any rotation in this block
lastaction...endscriptRun after all rotations in this block

Essential Commands

CommandWhat it does
sudo logrotate -d /etc/logrotate.d/myappDry run — show what would happen
sudo logrotate -v /etc/logrotate.d/myappVerbose run
sudo logrotate -f /etc/logrotate.d/myappForce rotation now
sudo logrotate -vf /etc/logrotate.d/myappForce with verbose output
sudo cat /var/lib/logrotate/logrotate.statusSee when each log was last rotated
ls /etc/logrotate.d/List all drop-in configs
systemctl status logrotate.timerCheck systemd timer status

← Back to Logging Index ↑ Back to EXPANDED