Skip to content

logrotate

logrotate renames, compresses and deletes log files on a schedule so that /var/log does not fill the disk. The interview point is what happens to a program that still holds the old file open, and how create, copytruncate and postrotate handle it.

Track: Core · Interview weight: Med


Must-Know Facts

Fact Value Verify with
Config /etc/logrotate.conf (globals), /etc/logrotate.d/<app> (per log) cat /etc/logrotate.conf
Schedule logrotate.timer, daily on both families (cron.daily on older systems) systemctl list-timers logrotate.timer
State file Last rotation per file: RHEL /var/lib/logrotate/logrotate.status, Ubuntu /var/lib/logrotate/status cat the file
Dry run logrotate -d <conf> changes nothing logrotate -d /etc/logrotate.conf
Force logrotate -f <conf> rotates even if not due logrotate -v -f /etc/logrotate.d/app
Frequency daily, weekly, monthly, yearly, or size 100M; maxsize combines both man logrotate.conf
Keep rotate 7 keeps seven old files ls /var/log/app
Naming Numbered (app.log.1) or dated with dateext (RHEL default) ls /var/log
create Rename the file, create a new empty one; the program must reopen postrotate block
copytruncate Copy then truncate in place; the program keeps its descriptor; lines written between copy and truncate are lost ls -l /proc/<pid>/fd
compress / delaycompress gzip old files; skip the newest one ls *.gz
postrotate ... endscript Command after rotation, usually a reload or kill -USR1 cat /etc/logrotate.d/nginx
sharedscripts Run postrotate once for all files that matched cat /etc/logrotate.d/nginx
su user group Rotate as that user; required when the directory is group- or world-writable logrotate -d warning

Defaults on Each Family

grep -v "^#" /etc/logrotate.conf | grep .
systemctl cat logrotate.timer | grep -E "^On|^Acc|^Rand|^Pers"

Output:

weekly
rotate 4
create
dateext
include /etc/logrotate.d
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
cat /etc/logrotate.conf | grep -v "^#" | grep .
logrotate -d /etc/logrotate.conf 2>&1 | grep -i "state" | head -2

Output:

weekly
su root adm
rotate 4
create
include /etc/logrotate.d
Reading state from file: /var/lib/logrotate/status
state file /var/lib/logrotate/status does not exist

The state file appears after the first scheduled run. Ubuntu has no dateext, so rotated files are numbered (syslog.1, syslog.2.gz), and the global su root adm matches the adm-group log files.

The vendor rule for nginx shows the usual pattern: rename, create a new file, then signal the master process to reopen its logs.

cat /etc/logrotate.d/nginx

Output:

/var/log/nginx/*.log {
    create 0640 nginx root
    daily
    rotate 10
    missingok
    notifempty
    compress
    delaycompress
    sharedscripts
    postrotate
        /bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true
    endscript
}

The Open File Descriptor Problem

A program that opened app.log holds a descriptor to the inode, not to the name. After create renames the file, the program keeps writing into app.log.1, and the new app.log stays empty. The demo below uses a small writer that never reopens its log.

cat /etc/logrotate.d/app
logrotate -v -f /etc/logrotate.d/app 2>&1 | tail -8
sleep 2
ls -l /var/log/app
ls -l /proc/$(systemctl show -p MainPID --value app-writer)/fd | grep app
tail -1 /var/log/app/app.log.1; wc -l < /var/log/app/app.log

Output:

/var/log/app/*.log {
    daily
    rotate 7
    missingok
    notifempty
    compress
    delaycompress
    create 0640 root root
}
old log /var/log/app/app.log.3.gz does not exist
renaming /var/log/app/app.log.2.gz to /var/log/app/app.log.3.gz (rotatecount 7, logstart 1, i 2), 
old log /var/log/app/app.log.2.gz does not exist
renaming /var/log/app/app.log.1.gz to /var/log/app/app.log.2.gz (rotatecount 7, logstart 1, i 1), 
old log /var/log/app/app.log.1.gz does not exist
log /var/log/app/app.log.8.gz doesn't exist -- won't try to dispose of it
renaming /var/log/app/app.log to /var/log/app/app.log.1
creating new /var/log/app/app.log mode = 0640 uid = 0 gid = 0
total 4
-rw-r----- 1 root root   0 Sep 17 05:53 app.log
-rw-r--r-- 1 root root 390 Sep 17 05:53 app.log.1
l-wx------ 1 root root 64 Sep 17 05:53 3 -> /var/log/app/app.log.1
05:53:27 request 20
0

The same thing makes a deleted log keep using disk: rm removes the name, the writer keeps the inode, and df does not drop until the program closes it. There are two fixes:

Fix How Trade-off
Signal the program to reopen postrotate with systemctl reload app or kill -USR1 Needs support in the program; no lost lines
Truncate in place copytruncate instead of create Works with any program; lines written during the copy are lost; the file must be opened with O_APPEND

After replacing create 0640 root root with copytruncate in the rule and restarting the writer, it keeps its descriptor on app.log:

logrotate -f /etc/logrotate.conf; ls -l /var/log/app
sleep 2
ls -l /var/log/app; ls -l /proc/$(systemctl show -p MainPID --value app-writer)/fd | grep app

Output:

total 8
-rw-r----- 1 root root  20 Sep 17 05:53 app.log
-rw-r----- 1 root root 190 Sep 17 05:53 app.log-20260917
total 8
-rw-r----- 1 root root 220 Sep 17 05:53 app.log
-rw-r----- 1 root root 190 Sep 17 05:53 app.log-20260917
l-wx------ 1 root root 64 Sep 17 05:53 3 -> /var/log/app/app.log

This time the file is named app.log-20260917, because the run used /etc/logrotate.conf, which sets dateext and then includes the app rule.

Running logrotate on one snippet ignores the global settings

logrotate -f /etc/logrotate.d/app produced app.log.1; logrotate -f /etc/logrotate.conf produced app.log-20260917. Test a rule through the main file, or repeat the needed globals inside the snippet.

copytruncate needs an appending writer

A program that writes at its own offset without O_APPEND continues at the old offset after truncation, leaving a sparse file that starts with NUL bytes. The writer above opens with mode a, so the new file starts cleanly.


Dry Runs and the State File

-d prints the decision for each file without changing anything. logrotate rotates only when the state file says the interval has passed.

grep -v "app" /var/lib/logrotate/logrotate.status | head -3; grep app /var/lib/logrotate/logrotate.status
logrotate -d /etc/logrotate.conf 2>&1 | grep -A4 "considering log /var/log/nginx/access.log"

Output:

logrotate state -- version 2
"/var/log/nginx/error.log" 2026-9-17-5:53:52
"/var/log/hawkey.log" 2026-9-17-5:53:52
"/var/log/app/app.log" 2026-9-17-5:53:52
considering log /var/log/nginx/access.log
  Now: 2026-09-17 05:53
  Last rotated at 2026-09-17 05:53
  log does not need rotating (log has already been rotated)
considering log /var/log/nginx/error.log

Common Errors

error: skipping "/var/log/app/app.log" because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.

Cause: The log directory is writable by a non-root group or by everyone, so rotating as root could be abused through symlinks.

Fix: Tighten the directory to 0755 root root, or add su <user> <group> to the rule. The captured case was chmod 775 /var/log/app; chgrp laborant /var/log/app.

warning: /tmp/bad.conf:4 unknown option 'copytruncte' -- ignoring line

Cause: A misspelled directive; logrotate skips the line and continues with the rest.

Fix: Run logrotate -d after every change and read the warnings.

error: /tmp/dup.conf:1 duplicate log entry for /var/log/app/app.log

Cause: Two rules match the same file.

Fix: Keep one rule per file; check with grep -r app.log /etc/logrotate.d.


Interview Checkpoints

L1: What is the difference between create and copytruncate?

Say first: create renames the file and makes a new one, so the program must reopen it; copytruncate copies and truncates in place, so the program keeps writing but a few lines can be lost.

Proof: ls -l /proc/<pid>/fd shows app.log.1 after create without a reload.

Follow-up: Which one does the nginx package use, and how does nginx reopen?

L1: How is logrotate scheduled on current RHEL and Ubuntu?

Say first: By logrotate.timer, daily, with Persistent=true.

Proof: systemctl list-timers logrotate.timer

Follow-up: Why does a weekly rule still wait a week if the timer runs daily? (The state file.)

L2: Rotate /var/log/app/*.log daily, keep 14 compressed copies, and reload the service afterwards.

Say first: One rule file with daily, rotate 14, compress, delaycompress, sharedscripts and a postrotate reload.

Proof: postrotate / systemctl reload app >/dev/null 2>&1 || true / endscript; then logrotate -d /etc/logrotate.conf.

Follow-up: Why delaycompress?

L2: Test a new rule without waiting for tomorrow.

Say first: Dry run, then force a rotation through the main config.

Proof: logrotate -d /etc/logrotate.conf; logrotate -v -f /etc/logrotate.conf

Follow-up: What does forcing do to every other log on the host?

L2: Rotate a log when it reaches 100 MB, even if the rule is daily.

Say first: Use maxsize 100M with daily, and run logrotate more often (an hourly timer).

Proof: maxsize 100M in the rule; systemctl edit logrotate.timer with OnCalendar=hourly.

Follow-up: How does size differ from maxsize? (size ignores the time interval.)

L3: After rotation, the application's log file stays at 0 bytes and the disk keeps filling. Why?

Say first: The program still writes to the renamed file because nothing told it to reopen.

Proof: ls -l /proc/<pid>/fd | grep log; lsof +L1 for deleted rotated files still open.

Follow-up: Fix with a postrotate reload, or copytruncate if the program cannot reopen.

L3: logrotate runs but one application's logs never rotate. What do you check?

Say first: The dry-run decision, the state file and warnings about permissions or duplicates.

Proof: logrotate -d /etc/logrotate.conf 2>&1 | grep -A5 app; grep app /var/lib/logrotate/logrotate.status; journalctl -u logrotate.

Follow-up: What does "parent directory has insecure permissions" require?


Captured on Rocky Linux 10.2 and Ubuntu 24.04.4 LTS (iximiuz Labs microVMs, kernel 6.1.167), 2026-09.