If you've ever cleared a disk-full alert on your VPS by truncating a 12GB Apache log, only to see it happen again three months later, the fix isn't cleaning up faster — it's making sure logs never grow unbounded in the first place. That's what logrotate does, and on most VPS setups it's either half-configured or silently broken for the one app that actually needs it.
Symptom: How You Find Out Logrotate Isn't Working
Nobody checks logrotate until something forces the issue. The usual triggers:
- A disk-usage alert fires, and
du -sh /var/log/*shows one file at several gigabytes /var/log/nginx/access.logor a custom app log has been growing since the server was provisioned, with no.1,.2.gzsiblings next to it- You run
logrotate -d /etc/logrotate.d/nginxand it errors out instead of showing a dry-run plan - Rotated files exist, but they're all dated the same week — rotation stopped working after a config change and nobody noticed
Logrotate is a cron-driven tool, not a real-time watcher, so it fails quietly. There's no crash, no alert — the log just keeps growing until something else (disk space, MySQL, an upload) breaks first.
Cause: Why Rotation Stops Happening
A handful of things break logrotate on a typical VPS, and they're rarely obvious from the outside:
- The app was installed outside the package manager. Nginx and Apache from your distro's repo ship a working
/etc/logrotate.d/entry. A Node app, a custom PHP daemon, or anything writing to/home/user/app/logs/app.logalmost never gets one automatically. - Cron isn't running logrotate at all. On most distros it's triggered by
/etc/cron.daily/logrotate— ifcronoranacronis disabled, stopped, or the daily job silently failed once and nobody re-ran it, rotation just doesn't happen. - A syntax error in one config file blocks the whole run. Logrotate processes
/etc/logrotate.d/*as one batch by default. One broken file with a stray brace can stop rotation for every other app on the box, not just the broken one. - The app doesn't reopen its log file after rotation. Logrotate renames the old file and creates a new one, but if the writing process still holds the old (now-renamed) file handle open, everything you write after that point goes into a file with no name — invisible to
ls, still eating disk, until the process restarts. - Size or time thresholds are wrong for the traffic you actually get. A default weekly rotation is fine for a low-traffic site; it's useless for a busy access log that hits gigabytes in two days.
Fix: Setting Up Logrotate Properly
1. Check what's already rotating
ls /etc/logrotate.d/
cat /etc/logrotate.conf
If your app's log directory isn't referenced anywhere here, it isn't being rotated — full stop, regardless of how big it's gotten.
2. Write a config for anything that's missing
Here's a config for a custom app logging to /home/user/app/logs/app.log, saved as /etc/logrotate.d/myapp:
/home/user/app/logs/app.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
}
What each line actually buys you:
| Directive | What it does |
|---|---|
daily | Rotate once a day rather than waiting for a size threshold |
rotate 14 | Keep 14 old copies, then delete the oldest automatically |
compress | cpanel" class="auto-link">Gzip old logs so they take a fraction of the space |
delaycompress | Leave the most recent rotated file uncompressed for one cycle, so anything still writing to it briefly doesn't fight the gzip process |
missingok | Don't error out if the log file doesn't exist that day |
notifempty | Skip rotation if the file is empty — no point creating zero-byte archives |
copytruncate | Copy the log then truncate the original in place, instead of renaming it — the safe option when the app can't be told to reopen its log file |
If the app can reopen its log on signal (Nginx and Apache both can), skip copytruncate and use a postrotate block instead — it's more efficient because nothing gets copied, just renamed:
/var/log/nginx/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
postrotate
systemctl reload nginx > /dev/null 2>&1 || true
endscript
}
copytruncate has a small window where log lines can be lost between the copy and the truncate — fine for a general access log, but avoid it for anything where you can't lose a line (audit logs, payment logs). Use postrotate + reload there instead.
3. Test before you trust it
Never wait for tomorrow's cron run to find out if a config is broken. Dry-run it:
# Shows what would happen, changes nothing
logrotate -d /etc/logrotate.d/myapp
# Actually forces rotation right now, ignoring the schedule
logrotate -f /etc/logrotate.d/myapp
Run the dry-run on the whole directory occasionally, too — it's the fastest way to catch a syntax error in one file before it silently blocks rotation for everything else:
logrotate -d /etc/logrotate.conf
4. Confirm cron is actually calling it
cat /etc/cron.daily/logrotate
systemctl status cron # or crond on AlmaLinux/Rocky
grep logrotate /var/log/syslog # or /var/log/cron on RHEL-based distros
If cron itself is down, no logrotate config — however well written — will ever run.
5. Size-based rotation for high-traffic logs
If an access log hits several gigabytes well before its weekly or daily rotation is due, add a size directive so it rotates on whichever comes first:
/var/log/nginx/access.log {
daily
rotate 7
size 100M
compress
missingok
notifempty
}
Prevention: Keep It From Silently Breaking Again
- Write a logrotate config the same day you deploy any new app that writes logs. It's a two-minute file, and it's much easier to write before the log is already 8GB.
- Check
logrotate -d /etc/logrotate.confafter any config change to any file in/etc/logrotate.d/— one bad edit can quietly stop rotation for every app on the server. - Set up a disk-usage alert so a rotation failure gets caught by a threshold warning instead of an outage.
- Match rotate counts to your actual audit/debug needs — keeping 90 days of debug-level logs "just in case" is usually just 90 days of disk you didn't need to spend.
- Test
postrotatereload commands after any web server or app upgrade. A changed service name or binary path can silently break the reload, so the app keeps writing to a renamed, deleted file handle without anyone noticing until disk fills up.
Logrotate is one of those tools that's invisible when it's working and expensive when it isn't. Give every log-writing process on your VPS an explicit config, test it with -d before you trust it, and you'll stop meeting this problem as a disk-full emergency.
Frequently Asked Questions
Do I need logrotate if my app already logs to journald or syslog?
Less urgently, but still worth checking. journald has its own size caps (SystemMaxUse in /etc/systemd/journald.conf), and syslog-forwarded logs still land in a flat file under /var/log that needs the same rotation rules as anything else.
What's the difference between copytruncate and the default rename-based rotation?
Default rotation renames the log file and signals the app to reopen a fresh one — no data loss, but it requires the app to support that signal. copytruncate copies the current contents out, then truncates the original file in place, which works with any app but has a brief window where a line or two written during the copy can be lost.
Why did my rotated log files stop compressing after a server migration?
Usually a missing gzip binary on the new server, or a permissions issue where the logrotate process can't write the compressed file to that directory. Run logrotate -d on the affected config — it'll print the exact error instead of failing silently.
Can I force logrotate to run right now instead of waiting for cron?
Yes — logrotate -f /etc/logrotate.d/yourconfig forces immediate rotation regardless of the schedule or size threshold, which is useful right after you've written a new config and want to confirm it actually works end to end.
How many days of logs should I actually keep?
For general access/error logs, 7-14 days covers most troubleshooting needs. Extend that only for logs you have a specific compliance or audit reason to keep longer — and push those to offsite storage rather than letting them sit uncompressed on the same disk as everything else.
