How to Rotate Nginx Log Files

How to Rotate Nginx Log Files

How to Rotate Nginx Log Files

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:

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

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

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

Performance Tips

Real-World Use Cases

Best Practices I Follow

  1. Always verify the postrotate reopen signal is actually working — don’t just trust the default config.
  2. Use delaycompress to avoid issues with tools reading logs immediately after rotation.
  3. Set per-site log files for any server hosting multiple domains, for easier debugging and analysis.
  4. Choose size-based rotation for high-traffic sites, time-based for lower-traffic ones.
  5. Test with logrotate -d before trusting any new configuration.
  6. Monitor disk usage on your log partition separately, so you catch rotation failures before they become emergencies.
  7. Consider shipping logs off-box for long-term retention rather than growing local retention indefinitely.
  8. 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.

Exit mobile version