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.log or a custom app log has been growing since the server was provisioned, with no .1, .2.gz siblings next to it
  • You run logrotate -d /etc/logrotate.d/nginx and 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.log almost never gets one automatically.
  • Cron isn't running logrotate at all. On most distros it's triggered by /etc/cron.daily/logrotate — if cron or anacron is 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:

DirectiveWhat it does
dailyRotate once a day rather than waiting for a size threshold
rotate 14Keep 14 old copies, then delete the oldest automatically
compresscpanel" class="auto-link">Gzip old logs so they take a fraction of the space
delaycompressLeave the most recent rotated file uncompressed for one cycle, so anything still writing to it briefly doesn't fight the gzip process
missingokDon't error out if the log file doesn't exist that day
notifemptySkip rotation if the file is empty — no point creating zero-byte archives
copytruncateCopy 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.conf after 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 postrotate reload 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.