Logs grow until they fill the disk unless something rotates and deletes them, and many compliance frameworks also require you to keep them for a minimum period. On Ubuntu 24.04 two systems handle this: logrotate for the plain text files in /var/log, and systemd-journald for the binary journal. In this tutorial you will review the defaults, define a retention policy, write a logrotate configuration for your own application, cap the size and age of the journal, and test everything before the next scheduled run.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - An application that writes log files to disk. This guide uses a hypothetical app called
myappthat writes to/var/log/myapp/and runs as themyappsystem user under a systemd service calledmyapp.service.
Step 1 - Deciding on a retention policy
Before editing any file, decide how long each type of log must be kept. Retention is a balance between what regulations or your incident response process need and the principle of not keeping personal data longer than necessary.
| Requirement | What it asks for |
|---|---|
| PCI DSS 4.0 (req. 10.5.1) | Keep audit logs for at least 12 months, with the last 3 months immediately available for analysis |
| GDPR (art. 5) | No fixed period: keep logs containing personal data (IP addresses, emails) only as long as the purpose requires |
| Incident response | Enough history to investigate an intrusion that is detected late, often 90 days or more |
A practical split for many servers is: keep web access logs 14 to 30 days on the server, keep authentication and audit logs 90 days on the server, and ship anything that must be kept for a year to a central log server or object storage. Local rotation handles the first part; this tutorial focuses on it.
Step 2 - Understanding how logrotate runs
logrotate is installed by default. On Ubuntu 24.04 it is triggered by a systemd timer, not by cron. Check the timer:
systemctl list-timers logrotate.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Fri 2026-09-26 00:00:00 UTC 9h left Thu 2026-09-25 00:00:04 UTC 14h ago logrotate.timer logrotate.service
The timer runs logrotate /etc/logrotate.conf once a day. That file sets global defaults and then includes every file in /etc/logrotate.d/, where each package drops its own policy:
ls /etc/logrotate.d/
alternatives apport apt bootlog btmp dpkg nginx rsyslog ubuntu-advantage-tools ufw unattended-upgrades wtmp
Look at the policy for the system logs written by rsyslog (/var/log/syslog, /var/log/auth.log and others):
cat /etc/logrotate.d/rsyslog
It rotates weekly and keeps 4 old files, so auth.log is kept for about a month by default. If your policy requires 90 days, change rotate 4 to rotate 13 in that file. Package upgrades will ask before overwriting a file you changed.
logrotate records when it last rotated each file in /var/lib/logrotate/status. That is how it knows whether a daily or weekly file is due.
Step 3 - Writing a policy for your application
Create a dedicated file for your application's logs:
sudo nano /etc/logrotate.d/myapp
/var/log/myapp/*.log {
daily
rotate 30
maxage 30
missingok
notifempty
compress
delaycompress
dateext
dateformat -%Y%m%d
create 0640 myapp adm
su myapp adm
sharedscripts
postrotate
systemctl kill -s HUP myapp.service >/dev/null 2>&1 || true
endscript
}
What each directive does:
dailyandrotate 30: rotate once a day and keep 30 old files, which gives 30 days of history.maxage 30: also delete rotated files older than 30 days, even if the count has not been reached (useful after gaps in rotation).missingokandnotifempty: do not fail if the log is missing, and skip rotation of empty files.compressanddelaycompress: gzip old logs, but leave the most recent rotated file uncompressed for one cycle, because some programs keep writing to it briefly.dateextanddateformat: name rotated filesapp.log-20260925instead ofapp.log.1, which makes it easy to find a given day.create 0640 myapp adm: recreate an empty log with these permissions and owner after rotating. Groupadmlets administrators read logs withoutsudo.su myapp adm: run the rotation as that user and group. logrotate refuses to work in a directory writable by a non-root user unless this is set.postrotate: tell the application to reopen its log file.sharedscriptsmakes this run once, not once per matched file.
The postrotate command only works if your application reopens its log files on SIGHUP. If it cannot, replace create, sharedscripts and the postrotate block with copytruncate. logrotate then copies the log and truncates the original in place. It works with any program, but lines written between the copy and the truncate are lost, so prefer the signal approach when the application supports it.
Step 4 - Testing the configuration
Never wait for the nightly run to find out a policy is broken. Run logrotate in debug mode against your file. Debug mode shows what would happen without changing anything:
sudo logrotate --debug /etc/logrotate.d/myapp
reading config file /etc/logrotate.d/myapp
...
rotating pattern: /var/log/myapp/*.log after 1 days (30 rotations)
empty log files are not rotated, old logs are removed
switching euid from 0 to 999 and egid from 0 to 4
considering log /var/log/myapp/app.log
Now: 2026-09-25 14:10
Last rotated at 2026-09-25 14:00
log does not need rotating (log has been already rotated)
Fix any error: line before continuing. Then force a real rotation once to confirm that permissions and the reload work:
sudo logrotate --force /etc/logrotate.d/myapp
ls -l /var/log/myapp/
-rw-r----- 1 myapp adm 0 Sep 25 14:12 app.log
-rw-r----- 1 myapp adm 5321 Sep 25 14:12 app.log-20260925
The new app.log should be empty with the expected owner, and your application should be writing to it again. Check with tail -f /var/log/myapp/app.log while you generate some traffic.
Step 5 - Rotating by size or more often than daily
Some logs grow too fast for a daily rotation. Add maxsize to rotate early when a file exceeds a size, in addition to the time schedule:
/var/log/myapp/*.log {
daily
maxsize 500M
rotate 30
...
}
maxsize is only evaluated when logrotate runs, which is once a day by default. To rotate large logs as soon as they cross the limit, run logrotate every hour by overriding the timer:
sudo systemctl edit logrotate.timer
Add these lines in the editor. The empty OnCalendar= clears the original daily schedule before setting the new one:
[Timer]
OnCalendar=
OnCalendar=hourly
Reload and confirm the new schedule:
sudo systemctl daemon-reload
systemctl list-timers logrotate.timer
The NEXT column should now show the next full hour. Files marked daily or weekly are still rotated only once per day or week, because logrotate checks the status file; the hourly run only makes maxsize and hourly policies effective.
Step 6 - Setting journald retention
Services managed by systemd also log to the journal in /var/log/journal/. By default journald uses up to 10% of the filesystem (capped at 4 GB) and has no time limit. Check the current usage:
journalctl --disk-usage
Archived and active journals take 1.2G in the file system.
Set explicit limits with a drop-in file:
sudo mkdir -p /etc/systemd/journald.conf.d
sudo nano /etc/systemd/journald.conf.d/retention.conf
[Journal]
SystemMaxUse=1G
SystemKeepFree=2G
MaxRetentionSec=90day
SystemMaxUse caps the total size, SystemKeepFree makes journald stop growing when free disk space falls under that value, and MaxRetentionSec deletes entries older than 90 days. Restart journald to apply it:
sudo systemctl restart systemd-journald
To free space immediately down to the new limits, vacuum the archived journals:
sudo journalctl --vacuum-size=1G --vacuum-time=90d
Run journalctl --disk-usage again to confirm the size dropped, and journalctl --list-boots | head -n 3 to see how far back the oldest entries go.
Troubleshooting
error: skipping "/var/log/myapp/app.log" because parent directory has insecure permissions: the directory is writable by a user or group other than root. Add asuline with the owning user and group, as in Step 3.- Rotated files keep growing and the new log stays empty: the application still has the old file open. Either make the
postrotatesignal work or switch tocopytruncate.sudo lsof +L1 | grep myappshows deleted files still held open. - Rotation never happens: check
sudo journalctl -u logrotate.servicefor errors from the last run. A syntax error in any file under/etc/logrotate.d/can stop the whole run, so test withsudo logrotate --debug /etc/logrotate.conf.
Conclusion
You now have a written retention policy, a tested logrotate configuration for your application, and a journal with a size and age limit, so logs can no longer fill the disk and are kept exactly as long as you decided. Next, consider shipping logs that must be kept longer to a central server with rsyslog or a log platform, protecting security-relevant events with auditd, and removing personal data such as full IP addresses from logs you retain for a long time.
