Logrotate for Nginx and App Logs on a Linux VPS
1/var/log/myapp/*.log {
2 daily
3 rotate 14
4 compress
5 delaycompress
6 missingok
7 notifempty
8 create 0640 www-data adm
9 sharedscripts
10 postrotate
11 systemctl reload myapp >/dev/null 2>&1 || true
12 endscript
13}
That single stanza is the whole job: it keeps two weeks of compressed application logs, never lets them grow without bound, and tells the app to reopen its log file cleanly after each rotation. Every directive in it earns its place, and getting one of them wrong — particularly the postrotate signal — is how a server ends up writing gigabytes into a "rotated" file nobody is reading. This guide explains the stanza line by line, applies the same pattern to nginx, and shows how to test a rule safely before it runs unattended at 3 a.m.
/etc/cron.daily (or a systemd timer) and processes drop-in rules in /etc/logrotate.d/. A good rule sets a retention count (rotate 14), compresses old logs (compress), and — the part most people get wrong — signals the writing process in a postrotate block so it reopens its log file instead of clinging to the renamed one. Always dry-run a new rule with logrotate -d before trusting it, and force a real run with logrotate -f to confirm.How Logrotate Actually Runs
Logrotate is not a daemon. It is a program that runs once a day — invoked by /etc/cron.daily/logrotate on most systems, or a logrotate.timer unit on newer ones — reads its configuration, and rotates any log that is due. The main config at /etc/logrotate.conf sets defaults and, critically, includes everything in the drop-in directory:
1include /etc/logrotate.d
That line is why you should never edit logrotate.conf for an individual service. Instead you drop a self-contained rule file into /etc/logrotate.d/, named for the service it governs. Packages do exactly this — installing nginx leaves a /etc/logrotate.d/nginx behind — and your own applications should follow the same convention. One file per service keeps rules isolated, readable, and trivially removable.
The Core Directives, Line by Line
Each directive in the opening stanza controls one aspect of rotation. Understanding them is what lets you write a rule with intent rather than copying one and hoping:
daily— rotate once a day. Alternatives areweekly,monthly, orsize 100Mto rotate when a file crosses a threshold regardless of the calendar.rotate 14— keep 14 old versions, then delete the oldest. This single number is your retention policy and the hard cap on how much disk the logs can ever occupy.compress— gzip rotated logs. Text logs compress by roughly 90%, so this is almost free space.delaycompress— wait one cycle before compressing, so the most recent rotated log stays plain text. This matters when a process keeps writing briefly after rotation; combined withcompressit avoids compressing a file still in use.missingok— do not error if the log file is absent (e.g. a service that has not logged yet). Without it, a missing file aborts the run.notifempty— skip rotation when the log is empty, avoiding a directory full of zero-byte archives.create 0640 www-data adm— after moving the old log aside, immediately create a fresh empty one with these permissions and ownership, so the service has a file to write to with the right access.sharedscripts— when the pattern matches several files, run thepostrotatescript once for the whole set rather than once per file.
The postrotate Signal — the Part People Get Wrong
Here is the directive that separates a working rule from one that silently fails. Rotation works by renaming the active log (access.log becomes access.log.1) and creating a new empty file. But a long-running process like nginx opened access.log by its inode, not its name — so after the rename it keeps writing to the renamed file, and your shiny new access.log stays empty while the "rotated" access.log.1 grows forever. The disk fills, and rotation appears to do nothing.
The fix is to tell the process to reopen its log files after rotation, via a postrotate script. Nginx reopens its logs on the USR1 signal, which is exactly what its packaged rule sends:
1postrotate
2 [ -f /run/nginx.pid ] && kill -USR1 "$(cat /run/nginx.pid)"
3endscript
For a service managed by systemd, the equivalent is a reload that triggers the same reopen:
1postrotate
2 systemctl reload nginx >/dev/null 2>&1 || true
3endscript
The || true keeps a transient reload failure from aborting the whole logrotate run. Whichever form you use, this block is mandatory for any log written by a persistent process — omit it and rotation is cosmetic. This is the same class of problem as a deleted-but-open file: the process is holding a file handle the filesystem can no longer see by name.
A Complete nginx Rule
Putting the pattern together for nginx's access and error logs gives a rule that mirrors what a well-configured server runs in production:
1/var/log/nginx/*.log {
2 daily
3 rotate 14
4 compress
5 delaycompress
6 missingok
7 notifempty
8 create 0640 www-data adm
9 sharedscripts
10 postrotate
11 [ -f /run/nginx.pid ] && kill -USR1 "$(cat /run/nginx.pid)"
12 endscript
13}
sharedscripts is doing real work here: the pattern matches both access.log and error.log, and without it the USR1 signal would be sent once per file — harmless for nginx but wasteful, and genuinely wrong for some services. With it, nginx is signalled exactly once after all matching logs have been rotated.
Test Before You Trust It
A logrotate rule runs unattended, so a mistake in it surfaces as a slow-motion disk-fill you discover days later. Two flags let you verify a rule the moment you write it. First, the debug run, which parses your config and prints exactly what it would do without changing a single file:
1sudo logrotate -d /etc/logrotate.d/nginx
Read the output: it names each log it considered, whether it is due for rotation, and which scripts it would run. A syntax error or a bad path shows up here, safely, before any file is touched. Once the dry run looks right, force an actual rotation to confirm the real thing works — including the postrotate signal — instead of waiting a day for the scheduled run:
1sudo logrotate -fv /etc/logrotate.d/nginx
-f forces rotation even if the logs are not yet due, and -v (verbose) narrates each step. Afterwards, check that a fresh empty log was created and the old one was renamed and compressed:
1ls -lh /var/log/nginx/
You should see a new access.log, a access.log.1, and older entries as .gz. If the new log is growing and the .1 is not, your postrotate signal is not reaching the process — go back and check it.
State Tracking and the Status File
One last thing worth knowing so a "broken" rule does not confuse you: logrotate records when it last rotated each file in a state file, normally /var/lib/logrotate/status (or /var/lib/logrotate.status). This is why running logrotate twice in one day does nothing the second time — it already rotated today — and why you need -f to force an out-of-schedule run during testing. If you ever wonder whether a log was rotated, that status file has the timestamp. Between a correct stanza, a working postrotate signal, and the -d/-f test cycle, your logs become a bounded, self-maintaining part of the system instead of the quiet reason a disk alert fires next month.
References
Posts in this series
- Linux VPS Performance Tuning: sysctl & swap 2026
- Systemd Service Hardening: Sandbox a Unit (2026)
- Fail2ban on Ubuntu VPS: Stop SSH Brute Force 2026
- UFW Firewall Rules for a Public VPS: 2026 Setup
- How to Harden SSH on an Ubuntu VPS (2026 Guide)
- Automated Backups with Rsync and Cron on Linux 2026
- TLS Certificates with Certbot on an Ubuntu VPS 2026
- Harden Nginx on Ubuntu: Headers, TLS & Limits 2026
- Automatic Security Updates on Ubuntu: 2026 Setup
- Logrotate for Nginx and App Logs on a Linux VPS
- Systemd Timers: Replace Cron Jobs on a Linux VPS
- Manage Linux Users and sudo Permissions on a VPS
- Linux VPS Disk and Inode Cleanup: du, find, lsof
- journald Logs: Retention, Size Limits and Queries