Server

logrotate: Configuration That Doesn't Lose Logs

By Prabath Thalangama· October 24, 2026· 3 min read
#linux#logrotate#troubleshooting

Introduction

logrotate keeps /var/log from filling the disk. The two things people get wrong: the app keeps writing to the old file after rotation (so the new file stays empty), and rotations silently stop happening. Both are easy to fix once you know why.

Concepts

logrotate is run by a daily cron job or systemd timer — it is not a daemon. Each run it reads /etc/logrotate.conf and everything in /etc/logrotate.d/, and for each log file decides whether it's due for rotation (by age or size), then renames/compresses/deletes according to the config.

The core problem: a long-running process holds an open file descriptor to /var/log/app.log. logrotate renames it to app.log.1. The process still writes to the same fd — now app.log.1 — and the fresh app.log stays empty.

Two solutions:

  • create + a postrotate signal — logrotate makes a new empty file, then runs a script that tells the process to reopen its logs (kill -HUP, systemctl reload, nginx -s reopen). Preferred: no lost lines, no race.
  • copytruncate — logrotate copies the file then truncates the original to zero. The process's fd still points at the same inode, now empty. Simple, but a small window exists where lines written during the copy are lost.

Hands-on

A typical /etc/logrotate.d/myapp:

/var/log/myapp/*.log {
    daily
    rotate 14                 # keep 14 old files
    missingok                 # don't error if the log is absent
    notifempty                # skip rotation if the file is empty
    compress
    delaycompress            # compress on the NEXT rotation (last file stays readable)
    dateext                  # app.log-20260827 instead of app.log.1
    dateformat -%Y%m%d
    sharedscripts            # run postrotate once, not per matched file
    postrotate
        systemctl reload myapp.service >/dev/null 2>&1 || true
    endscript
}

Or the copytruncate variant (for an app you can't signal):

/var/log/legacyapp/app.log {
    daily
    rotate 7
    compress
    delaycompress
    copytruncate
    missingok
    notifempty
}

Size-based rotation

/var/log/chatty.log {
    size 100M                 # rotate when it hits 100M (checked each logrotate run)
    rotate 5
    compress
    missingok
}

size alone means "only on size"; combine with daily + maxsize 100M to rotate daily or sooner if it gets big.

Verification and troubleshooting

# Force a run of one config, verbose, ignoring the "not due yet" check
sudo logrotate -f -v /etc/logrotate.d/myapp

# Dry run — show what it WOULD do
sudo logrotate -d /etc/logrotate.d/myapp

# When did each log last rotate? (the state file)
sudo cat /var/lib/logrotate/logrotate.status
  • New log file stays empty after rotation — the app still holds the old fd. Add a postrotate that reloads it, or use copytruncate. lsof /var/log/myapp/app.log* shows which process has the deleted file.
  • Rotation never happens — the timer/cron isn't running (systemctl status logrotate.timer), or a syntax error in one config file aborts the whole run (logrotate -d /etc/logrotate.conf shows the parse error), or the file isn't old enough / is empty (notifempty).
  • "error: skipping ... because parent directory has insecure permissions" — logrotate refuses world-writable log dirs unless you add su <user> <group> to the stanza or fix the directory perms.
  • Same log rotated twice / by two configs — a distro package config and your own both match the glob. logrotate warns "duplicate log entry"; remove one.
  • postrotate runs per file, reloading the service 10 times — add sharedscripts.
  • Compressed logs but the current one is also gzipped and unreadable — you want delaycompress so the most recent rotation stays plain text for a day.
  • Disk still fills — rotate N keeps N copies; if the log grows faster than daily rotation clears space, add size/maxsize and lower rotate, or fix the app's log verbosity.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.