A few years ago, I got paged at 2 AM because a production server had run out of disk space. The culprit wasn’t a runaway database or a leaked file upload — it was /var/log/nginx/access.log, which had quietly grown to over 40GB because log rotation had silently stopped working after a config change months earlier. That incident taught me to never treat log rotation as a “set it and forget it” thing. In this guide, I’ll walk through exactly how Nginx log rotation works, how to configure it properly with logrotate, and how to verify it’s actually doing its job.
Why Log Rotation Matters
Nginx, by default, logs every request to access.log and every error to error.log. On a busy server, these files can grow enormously fast — I’ve seen access logs balloon by gigabytes per day on high-traffic sites. Without rotation:
- Disk space fills up, eventually crashing your server or database
- Log analysis tools become slow or unusable against massive files
- You lose the ability to cleanly archive/compress historical logs
- Grepping through logs for debugging becomes painfully slow
Log rotation solves this by periodically renaming the current log file, starting a fresh one, compressing old logs, and deleting logs past a certain age — all without requiring Nginx downtime.
How Nginx Logging Works
By default, Nginx writes to two log files, defined in nginx.conf or per server block:
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
Nginx keeps a file descriptor open to these files for the lifetime of its worker processes. This detail matters a lot for rotation — if you simply rename or delete the log file, Nginx keeps writing to the same (now unlinked) file descriptor, and the space never actually gets freed until Nginx is told to reopen its log files.
Requirements
- Nginx installed (log rotation instructions apply the same whether Nginx was installed via apt, yum, or compiled from source)
logrotateinstalled (present by default on virtually every Linux distribution)- Root or sudo access
Step 1: Check the Default logrotate Configuration
On most distributions, installing Nginx via the package manager also drops a default logrotate config at /etc/logrotate.d/nginx. Let’s look at it:
cat /etc/logrotate.d/nginx
A typical default configuration looks like this:
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi \
endscript
postrotate
invoke-rc.d nginx rotate >/dev/null 2>&1
endscript
}
This is a solid starting point, but I almost always customize it for the specific server I’m managing. Let’s go through each directive.
Step 2: Understanding Each logrotate Directive
daily— Rotate logs once per day. Other options:weekly,monthly, orsize 100Mto rotate based on file size instead of time.missingok— Don’t throw an error if the log file is missing (useful if a site was recently removed).rotate 14— Keep 14 rotated log files before deleting the oldest. Withdaily, this gives you two weeks of history.compress— Gzip rotated logs to save disk space.delaycompress— Delay compression of the most recently rotated log by one cycle. This matters because some processes (or log-tailing tools) might still be reading the previous log file right after rotation; delaying compression avoids issues with tools trying to read a.gzfile mid-flight.notifempty— Don’t rotate the log if it’s empty — no point creating empty archive files.create 0640 www-data adm— After rotation, create a new empty log file with these permissions and ownership. Adjustwww-datatonginxon CentOS/RHEL systems.sharedscripts— Run thepostrotatescript only once for all matched log files, not once per file (important sinceaccess.loganderror.logare both matched by the glob).postrotate/invoke-rc.d nginx rotate— This is the critical part. After rotation, Nginx needs to be told to reopen its log file handles, since it’s still writing to the old (now renamed) file otherwise.
Step 3: Understanding the postrotate Reopen Signal
The postrotate script needs to signal Nginx to reopen log files. There are a few equivalent ways to do this depending on your distro:
Using the Nginx binary directly:
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
fi
endscript
USR1 is the signal Nginx’s master process listens for specifically to reopen log files without restarting worker processes — zero downtime, zero dropped connections.
Using systemctl (works well on systemd-based distros):
postrotate
systemctl reload nginx > /dev/null 2>&1 || true
endscript
A full reload also triggers a graceful reopening of log files as part of its normal reload process, along with re-reading the config — so this works too, though it’s marginally heavier than a plain USR1 signal.
I personally prefer the direct kill -USR1 approach because it’s the most surgical — it only reopens logs, without touching worker processes or config parsing.
Step 4: Writing a Custom logrotate Configuration
Here’s the configuration I typically deploy for a production Nginx server handling meaningful traffic:
sudo nano /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
fi
endscript
}
I bumped rotate from 14 to 30 days for better historical coverage, useful for debugging incidents that get reported a few weeks after the fact.
For high-traffic sites where daily rotation isn’t enough, I switch to size-based rotation instead:
/var/log/nginx/*.log {
size 500M
missingok
rotate 20
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
fi
endscript
}
This rotates whenever the log hits 500MB, regardless of time elapsed, which prevents any single day’s traffic spike from producing an unmanageably large file.
Step 5: Per-Site Log Files
If you’re running multiple virtual hosts, I like to configure separate log files per site rather than dumping everything into one shared access.log:
server {
server_name site1.example.com;
access_log /var/log/nginx/site1.access.log;
error_log /var/log/nginx/site1.error.log;
}
server {
server_name site2.example.com;
access_log /var/log/nginx/site2.access.log;
error_log /var/log/nginx/site2.error.log;
}
The wildcard glob /var/log/nginx/*.log in your logrotate config automatically picks up new site log files as you add them — no config change needed on the logrotate side.
Step 6: Testing Log Rotation
Never assume your logrotate config works — always test it. Use the -d (debug/dry-run) flag first:
sudo logrotate -d /etc/logrotate.d/nginx
This shows exactly what logrotate would do without actually doing it — check the output carefully for any errors about permissions or missing files.
To force an actual rotation right now for testing:
sudo logrotate -f /etc/logrotate.d/nginx
Then verify:
ls -lh /var/log/nginx/
You should see something like:
access.log
access.log.1
error.log
error.log.1
Confirm Nginx is writing to the new access.log, not the rotated one, by generating a test request and tailing the file:
curl http://localhost > /dev/null
tail -n 1 /var/log/nginx/access.log
If the new request doesn’t appear in the new log file, the postrotate reopen signal isn’t working — this is the single most common rotation bug.
Troubleshooting Common Issues
Disk usage doesn’t drop after rotation — This is the classic symptom of a missing or broken postrotate reopen signal. Nginx keeps its old file descriptor open even after the file is renamed, so space isn’t reclaimed until Nginx reopens (or you restart it). Check with:
sudo lsof | grep deleted | grep nginx
If you see entries marked (deleted), Nginx is still writing to unlinked file data — confirming the reopen signal never fired.
“error: nginx.pid not found” — The PID file path in your postrotate script doesn’t match your actual Nginx PID file location. Check:
cat /etc/nginx/nginx.conf | grep pid
Adjust the path in your logrotate script to match.
Permission denied errors during rotation — Usually means the create directive’s specified user/group doesn’t have write access to the log directory, or your Nginx worker processes run as a different user than what’s specified.
Logs not rotating at all — Check that logrotate itself is actually running via cron/systemd timer:
systemctl list-timers | grep logrotate
cat /etc/cron.daily/logrotate
Security Considerations
- Restrict log file permissions. Access logs can contain sensitive data (IPs, query strings, sometimes even tokens if poorly designed apps leak them into URLs). I use
0640permissions with a restricted group, not world-readable. - Don’t log sensitive query parameters. If your app passes tokens or passwords in URLs (it shouldn’t, but it happens), consider a custom
log_formatthat masks or omits sensitive fields. - Archive logs securely. If shipping rotated logs off-server for long-term storage (S3, a log aggregation service), ensure the transport is encrypted and access is restricted.
- Set a reasonable retention period — balancing compliance/debugging needs against unnecessary long-term storage of potentially sensitive access data.
Performance Tips
- Use
delaycompressto avoid compression contention right at rotation time when the previous log might still be in use by log-shipping tools. - If you run log analysis tools (GoAccess, AWStats) against live logs, make sure they also handle the file-reopen correctly, or point them at already-rotated, compressed files instead.
- For very high-traffic servers, consider shipping logs to a centralized system (ELK stack, Loki, or a hosted service) and keeping only a short local retention window — this reduces local disk pressure significantly.
Real-World Use Cases
- Compliance requirements: Some industries require access logs retained for a minimum period (90 days, a year, etc.) — I adjust
rotatecounts and combine with off-server archival to meet these requirements. - Debugging historical incidents: Having 30 days of compressed logs readily available has saved me multiple times when a client reports “this happened last week” and I need to trace it.
- Cost control on cloud storage: On servers where disk is billed per GB, aggressive but sensible rotation keeps costs predictable.
Best Practices I Follow
- Always verify the
postrotatereopen signal is actually working — don’t just trust the default config. - Use
delaycompressto avoid issues with tools reading logs immediately after rotation. - Set per-site log files for any server hosting multiple domains, for easier debugging and analysis.
- Choose size-based rotation for high-traffic sites, time-based for lower-traffic ones.
- Test with
logrotate -dbefore trusting any new configuration. - Monitor disk usage on your log partition separately, so you catch rotation failures before they become emergencies.
- Consider shipping logs off-box for long-term retention rather than growing local retention indefinitely.
- Restrict permissions on log files and rotated archives — they often contain more sensitive data than people assume.
Wrapping Up
Log rotation is one of those unglamorous pieces of server administration that nobody thinks about until it breaks — and when it breaks, it breaks badly, usually as a full disk at the worst possible time. Setting it up correctly takes fifteen minutes; verifying it actually works takes another five. Given how catastrophic the failure mode is, that’s about the best time investment you can make on a production Nginx server. Go check your own logrotate config right now — I’d bet a decent number of readers will find it’s not quite doing what they assumed.