Set Up logrotate on a VPS Before Logs Fill Your Disk
You free up disk space, restart the site, and three weeks later you're back at 100% full again. Nine times out of ten the culprit isn't backups or uploads — it's a log file that's been growing unchecked since the server was provisioned, because nobody ever told logrotate about it. Here's how to actually set it up so this stops happening.
Symptom: the disk fills up again and again
This one has a pattern. You run df -h, find / at 100%, clear some space, and everything's fine — until it isn't, a few weeks later. Common signs you're dealing with an unrotated log rather than a one-off:
du -sh /var/log/*shows one file that's several GB while everything else is tiny- A custom app log (Laravel's
storage/logs/laravel.log, a Node app'sapp.log, PHP'serror_log) is the single biggest thing on the disk - MySQL's slow query log or general log has been on since a debugging session months ago and never got turned off
journalctl --disk-usagereports several GB for systemd's journal
Find the actual offender before doing anything else:
du -ah /var/log 2>/dev/null | sort -rh | head -20
du -ah /home/*/logs /home/*/public_html 2>/dev/null | sort -rh | head -20
Cause: logrotate doesn't know about every log on your box
Most distros ship logrotate already installed and running daily via cron or a systemd timer, and it handles the standard system logs in /var/log out of the box — syslog, auth.log, apt history, that kind of thing. What it does not automatically handle:
- Application logs your own code writes to a custom path (Laravel, a Node/PM2 process, a Python app under Gunicorn)
- MySQL's slow query log or general query log if you turned them on for debugging
- Nginx or Apache vhost-specific logs you added manually outside the default config
- Docker container logs, which logrotate doesn't touch at all unless you configure the Docker daemon separately
Anything not covered by a logrotate config file just grows forever. There's no default cap.
Check what's already being rotated
# See every active logrotate config
cat /etc/logrotate.conf
ls /etc/logrotate.d/
# Dry-run to see what WOULD happen without actually rotating anything
logrotate -d /etc/logrotate.conf
If your app's log path doesn't show up anywhere in /etc/logrotate.d/, that's your answer.
Fix: write a config for the logs that are missing
Every logrotate config follows the same shape: which file(s), how often, how many to keep, and what to do after rotating. Create a new file per service in /etc/logrotate.d/ — don't edit the main logrotate.conf directly.
Example: a custom application log
sudo nano /etc/logrotate.d/myapp
/home/deploy/myapp/storage/logs/laravel.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 deploy deploy
sharedscript
postrotate
systemctl reload php8.2-fpm.service > /dev/null 2>&1 || true
endscript
}
What each directive is actually doing:
| Directive | What it does |
|---|---|
daily | Rotate once a day (also: weekly, monthly, or size 50M to trigger by size instead of time) |
rotate 14 | Keep 14 old copies before deleting the oldest |
compress | Gzip rotated files to save space |
delaycompress | Don't compress the most recent rotation — keeps it readable if a process still has it open |
missingok | Don't error out if the log file doesn't exist yet |
notifempty | Skip rotation if the file is empty — avoids piling up empty .gz files |
create 0640 deploy deploy | Recreate the log file with these permissions/owner right after rotating |
postrotate / endscript | Command to run after rotation — usually reloading the service so it reopens the new file handle |
Example: MySQL slow query log
If you turned on slow query logging to chase down a performance issue and forgot to turn it off, add:
/var/log/mysql/mysql-slow.log {
weekly
rotate 4
compress
missingok
notifempty
create 0640 mysql mysql
postrotate
mysqladmin flush-logs 2>/dev/null || true
endscript
}
MySQL needs the flush-logs call after rotation, otherwise it keeps writing to the now-renamed file instead of the fresh one — the log file gets deleted by rotation but the disk space isn't freed until the process stops holding it open.
Test before you trust it
Never wait for the daily cron to find out if your config is broken. Force it:
# Dry run - shows what would happen, changes nothing
logrotate -d /etc/logrotate.d/myapp
# Force an actual rotation right now, ignoring the schedule
sudo logrotate -f /etc/logrotate.d/myapp
# Confirm the new file exists with the right permissions
ls -la /home/deploy/myapp/storage/logs/
If the dry run errors on syntax, logrotate tells you the exact line. Fix it there before forcing a real run.
Systemd journal has its own limit
The journal isn't managed by logrotate at all — it's capped separately in /etc/systemd/journald.conf:
sudo nano /etc/systemd/journald.conf
# Set:
# SystemMaxUse=500M
sudo systemctl restart systemd-journald
# Or just trim it immediately without changing the permanent config:
sudo journalctl --vacuum-size=200M
Prevention: don't wait for the disk to fill up again
- Whenever you deploy something new that writes its own log file — a queue worker, a cron script, a new app — add a logrotate config for it in the same PR/deploy, not as an afterthought later
- Turn off MySQL's general query log and slow query log once you're done debugging with them — they're not meant to run permanently
- Set up basic disk alerting (Netdata, a simple cron +
dfthreshold script, or your monitoring tool of choice) so you get warned at 80% instead of finding out at 100% - If you're running Docker, configure log rotation on the daemon itself — logrotate can't see inside containers. Add
{"log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "3"}}to/etc/docker/daemon.jsonand restart Docker - Re-run
logrotate -d /etc/logrotate.confevery few months as a sanity check — new services have a habit of adding logs nobody configured
A VPS that's been running for a year without anyone touching logrotate configs is almost always sitting on a few gigabytes of log bloat waiting to be found. Ten minutes writing a config file now saves you an emergency cleanup later.
Frequently asked questions
How often does the default logrotate cron actually run?
On most distros it's triggered daily by /etc/cron.daily/logrotate or a systemd timer (check `systemctl list-timers | grep logrotate`). It only rotates files listed in /etc/logrotate.conf and /etc/logrotate.d/ — anything not listed there is never touched, no matter how often the cron fires.
Why did the log file's disk space not actually free up after rotation?
If a running process still has the old file open (common with MySQL, Nginx, or a long-running Node/PHP worker), the disk space isn't released until that process closes the file handle. That's what postrotate hooks like `systemctl reload` or `mysqladmin flush-logs` are for — they tell the service to reopen its log file after rotation.
Should I use size-based or time-based rotation?
Time-based (daily/weekly) is predictable and easier to reason about for most app logs. Use size-based (`size 50M`) for logs with unpredictable volume, like a debug log that might be silent for weeks then flood during an incident — you don't want it growing unbounded between scheduled rotations.
Can I rotate multiple log files with one config block?
Yes — list multiple paths separated by spaces or newlines inside the same block, e.g. `/var/log/app/*.log { ... }`. Add `sharedscript` if you want the postrotate command to run once total instead of once per matched file.
Does logrotate handle the systemd journal too?
No, the journal is managed separately through /etc/systemd/journald.conf (SystemMaxUse, SystemKeepFree) or ad-hoc with `journalctl --vacuum-size=`. If journalctl --disk-usage shows several GB, that's a journald setting, not a logrotate config.