Log Rotation with logrotate
Logs are useful, but they never stop growing. A busy web app can write hundreds of megabytes a day, and a single log file that is never trimmed will quietly eat your entire disk. When the disk hits 100%, your app cannot write files, your database may refuse new connections, and the server can become impossible to log into. Log rotation is the standard cure: it renames the current log, starts a fresh empty one, compresses the old ones, and deletes anything past a retention limit. On Ubuntu, the tool that does this is logrotate.
What log rotation is
Think of log rotation like a stack of notebooks. When the current notebook (your active log) gets full or old, you set it aside, label it with a date, and grab a brand-new blank one. You keep the last few old notebooks in a drawer, and you throw away anything older than that. Log rotation does exactly this, automatically, on a schedule.
A typical rotation turns one file into a numbered, compressed history:
app.log <- the live file your app writes to right now
app.log.1 <- yesterday's log
app.log.2.gz <- two days ago, compressed
app.log.3.gz <- three days ago, compressed
The full-disk incident. In the System Administration section we walked through a server that went down because
/var/log/myapp/app.loghad grown to 40 GB and filled the root partition. The fix in the moment was to truncate the file, but the real fix is rotation: a 10-line logrotate config would have kept that file under control forever. Disk-full outages caused by unrotated logs are one of the most common — and most preventable — incidents in operations.
How logrotate runs on Ubuntu
logrotate is a small program that reads config files, decides which logs are due for rotation, and acts on them. It does not run continuously. Instead it is triggered once a day by a systemd timer (a scheduled job managed by systemd, the program that starts and supervises services on Ubuntu). On a fresh Ubuntu 22.04 or 24.04 server, logrotate is already installed. You can confirm it and see the timer:
# Confirm logrotate is installed
logrotate --version
# See the timer that fires logrotate daily
systemctl list-timers logrotate.timer
Output:
logrotate 3.21.0
NEXT LEFT LAST UNIT ACTIVATES
Tue 2026-06-16 00:00:00 UTC 8h left Mon 2026-06-15 00:00:00 UTC logrotate.timer logrotate.service
The main config file is /etc/logrotate.conf, which holds global defaults. Individual programs drop their own rules into the /etc/logrotate.d/ directory — one small file per app. This is where you put your own configs. Anything in /etc/logrotate.d/ is automatically included, so you never edit the system’s files; you just add your own.
When to use logrotate (and when not to)
Use logrotate for any program that writes to its own plain-text file under /var/log/ — your app, Nginx, Apache, PostgreSQL, and so on. It is the right tool whenever you control a growing file on disk. Do not use logrotate for logs that go to the systemd journal (journalctl): the journal manages its own size and retention through journald, configured in /etc/systemd/journald.conf with settings like SystemMaxUse=500M. Pointing logrotate at journal files would fight that system. In short: file on disk → logrotate; journal → journald.
Writing a config for an app log
Suppose your app writes to /var/log/myapp/app.log and /var/log/myapp/error.log. Create a dedicated config file for it:
sudo nano /etc/logrotate.d/myapp
Put this inside:
/var/log/myapp/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 myapp myapp
sharedscripts
postrotate
systemctl reload myapp >/dev/null 2>&1 || true
endscript
}
Every line earns its place. Here is what each directive does:
| Directive | What it does | Why it matters |
|---|---|---|
daily | Rotate once per day | Sets the rotation frequency (alternatives: weekly, monthly, or size 100M) |
rotate 14 | Keep 14 old files, then delete | Your retention window — here, two weeks of history |
compress | Gzip rotated files | Old logs shrink ~90%, saving disk |
delaycompress | Wait one cycle before compressing | Keeps the most recent rotated file readable for quick tail |
missingok | Don’t error if the file is absent | Avoids noise when an app hasn’t logged yet |
notifempty | Skip rotation if the log is empty | No pointless empty .gz files |
create 0640 myapp myapp | Recreate the fresh log with these permissions/owner | Ensures the app can keep writing |
sharedscripts | Run postrotate once, not per file | The wildcard matches two files, but reload once |
postrotate ... endscript | Shell commands to run after rotation | Tells the app to reopen its log files |
Why postrotate matters
When logrotate renames app.log to app.log.1, your app may still be holding the old file open and happily writing to the renamed file — which means the new app.log stays empty. The fix is the postrotate block: it runs a command after rotation to nudge the app into reopening its files. For a systemd service that supports it, systemctl reload myapp is the clean way. Nginx and Apache do this automatically via their own configs (/etc/logrotate.d/nginx, /etc/logrotate.d/apache2), which already ship with the right postrotate lines.
Gotcha: if your app does not reopen log files on reload, use
copytruncateinstead ofcreate+postrotate. It copies the log, then truncates the original in place, so the app’s open file handle keeps working. The trade-off is a tiny window where log lines written during the copy can be lost.
Testing your config safely
Never wait until midnight to find out your config is broken. logrotate has a dry-run mode that prints exactly what it would do without touching anything:
# Check syntax and show the plan, change nothing
sudo logrotate --debug /etc/logrotate.d/myapp
Output:
reading config file /etc/logrotate.d/myapp
Handling 1 logs
rotating pattern: /var/log/myapp/*.log after 1 days (14 rotations)
considering log /var/log/myapp/app.log
log needs rotating
rotating log /var/log/myapp/app.log, log->rotateCount is 14
renaming /var/log/myapp/app.log to /var/log/myapp/app.log.1
running postrotate script
Once the dry run looks right, you can force a real rotation immediately to prove it end to end (the -f flag means “force, even if not yet due”):
sudo logrotate -f /etc/logrotate.d/myapp
ls -lh /var/log/myapp/
Output:
total 8.0K
-rw-r----- 1 myapp myapp 0 Jun 16 09:14 app.log
-rw-r----- 1 myapp myapp 1.2K Jun 16 09:14 app.log.1
logrotate also keeps a small state file at /var/lib/logrotate/status recording when each log was last rotated, so it never rotates twice in one day.
Rotating by size instead of time
Time-based rotation (daily) is fine for steady traffic, but a sudden flood of errors can fill a disk between daily runs. For high-volume or bursty logs, rotate by size, or combine both:
/var/log/myapp/access.log {
size 100M
rotate 7
compress
missingok
notifempty
copytruncate
}
Here the log rotates whenever it crosses 100 MB, regardless of the clock, keeping the last 7 chunks. Because the daily timer is what checks the size, pair size-based rules with a more frequent check (or an hourly cron entry) if your logs can grow that fast within a day.
Best Practices
- One config file per app in
/etc/logrotate.d/— never edit the bundled OS files; add your own. - Always
--debugfirst, then-fto test, before trusting a new config in production. - Set retention deliberately with
rotate N— match it to how long you actually need to investigate incidents, not “forever”. - Compress old logs to reclaim ~90% of the space, and use
delaycompressso the newest rotated file stays instantly readable. - Handle the open-file problem: use
postrotate+ reload for apps that reopen files, orcopytruncatefor those that don’t. - Don’t rotate journald logs with logrotate — cap them in
/etc/systemd/journald.confinstead. - Monitor disk usage anyway; rotation is your safety net, not a reason to stop watching
/var/logwith alerts.