Persistent journals and log rotation
By default journald is volatile: logs vanish on reboot unless Storage=persistent is set in journald.conf. SystemMaxUse caps disk use. journalctl --vacuum-size/--vacuum-time trims old data. logrotate handles the plain-text logs apps still write to /var/log.
A service crashes at 3 am, the on-call engineer reboots the box, and by morning the logs are gone. Not rotated — gone. The journal was volatile: it lived in /run/log/journal, which is a tmpfs that vanishes on every reboot. This is the default on many Debian and Ubuntu installations. The fix is a single config line, but if you do not know the default you will lose evidence every time. The second failure mode is the opposite: you make the journal persistent, set no size cap, and six months later the disk is full because SystemMaxUse was never configured. Both disasters are preventable once you understand the volatile/persistent split and how to size and trim the journal.
After this lesson you can distinguish volatile from persistent journal storage, enable persistence with Storage=persistent, set size caps via SystemMaxUse, trim existing journal data with --vacuum-size and --vacuum-time, and configure logrotate for plain-text logs that apps write to /var/log.
Volatile vs persistent: where does the journal live?
journald stores entries in one of two places depending on configuration:
# Check where the journal currently lives:
ls /run/log/journal/ # volatile — lost on reboot (tmpfs)
ls /var/log/journal/ # persistent — survives reboot
# Check the active storage mode:
journalctl --disk-usage
# Archived Journals: 0 B
# Active Journals: 48.0 M
# On a system with volatile storage only:
# /var/log/journal/ does NOT exist
# All data lives in /run/log/journal/ (tmpfs, lost on reboot)
# Verify the current Storage setting:
grep -i storage /etc/systemd/journald.conf 2>/dev/null || \
grep -i storage /etc/systemd/journald.conf.d/*.conf 2>/dev/null || \
echo "Storage= not set — default is auto"The default Storage=auto means: use persistent if /var/log/journal/ already exists, otherwise use volatile. On a fresh Ubuntu install /var/log/journal/ typically does not exist, so you are volatile by default.
Enable persistent storage.
One line in journald.conf and a directory creation:
# Edit journald configuration:
sudo mkdir -p /etc/systemd/journald.conf.d/
sudo tee /etc/systemd/journald.conf.d/persistence.conf <<'EOF'
[Journal]
Storage=persistent
EOF
# Create the persistent storage directory (journald will use it on restart):
sudo mkdir -p /var/log/journal
# Apply the change (no reboot needed):
sudo systemctl restart systemd-journald
# Verify:
journalctl --disk-usage
# Archived Journals: 0 B
# Active Journals: 52.0 M ← now on-disk in /var/log/journal/
ls /var/log/journal/
# <machine-id>/ ← one directory per machine IDAfter enabling persistence, logs from the next boot and all subsequent boots survive reboots. Logs from before the change are not retroactively saved.
Capping journal size with SystemMaxUse.
Without a size cap, a persistent journal will grow until it fills the disk. journald has built-in defaults (10% of the filesystem or 4 GB, whichever is smaller), but on a small VPS that default can still eat critical space.
# Set explicit size caps in the config drop-in:
sudo tee /etc/systemd/journald.conf.d/size.conf <<'EOF'
[Journal]
SystemMaxUse=500M
SystemKeepFree=200M
RuntimeMaxUse=100M
EOF
# SystemMaxUse — max total disk the journal may use
# SystemKeepFree — always keep this much free on the partition
# RuntimeMaxUse — cap for volatile (/run) journal
sudo systemctl restart systemd-journald
# Check current disk usage after restart:
journalctl --disk-usage
# Archived Journals: 320.1 M
# Active Journals: 48.0 M
# Total: 368.1 M ← within the 500M capjournald enforces the cap by rotating and deleting the oldest archived journal files first. You do not need to run any cleanup command manually — enforcement is continuous.
Manual vacuum: trim old journal data on demand.
When you need to reclaim space immediately (e.g. after a logging storm), use --vacuum-size or --vacuum-time:
# Remove archived journals until total usage is under 200M:
sudo journalctl --vacuum-size=200M
# Vacuuming done, freed 148.3M of archived journals
# Remove journals older than 2 weeks:
sudo journalctl --vacuum-time=2weeks
# Remove journals older than 30 days:
sudo journalctl --vacuum-time=30d
# Combine both (applies whichever is more restrictive):
sudo journalctl --vacuum-size=500M --vacuum-time=30d
# Verify space after vacuum:
journalctl --disk-usageVacuum only removes archived (rotated) journals, never the active one. It is safe to run while the system is live. A common pattern: run --vacuum-time=30d weekly from a systemd timer or cron job.
logrotate for plain-text logs in /var/log.
Not everything goes through journald. Apps like nginx, PostgreSQL, and many legacy daemons still write to files under /var/log. logrotate handles these.
# logrotate is driven by /etc/logrotate.conf and /etc/logrotate.d/:
cat /etc/logrotate.d/nginx
# /var/log/nginx/*.log {
# daily
# missingok
# rotate 14
# compress
# delaycompress
# notifempty
# create 0640 www-data adm
# sharedscripts
# postrotate
# /bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true
# endscript
# }
# Key directives:
# daily/weekly/monthly — rotation frequency
# rotate 14 — keep 14 rotated files, then delete
# compress — gzip rotated files (saves ~90% space)
# delaycompress — do not compress the most recent rotated file
# (nginx may still be writing to it)
# postrotate/endscript — run this shell block after rotation
# (sends USR1 to nginx to reopen log files)
# Test logrotate without actually rotating (dry run):
sudo logrotate --debug /etc/logrotate.d/nginx
# Force rotation immediately (ignores time check):
sudo logrotate --force /etc/logrotate.d/nginxThe postrotate block is critical: apps that hold a file descriptor open will keep writing to the old (now renamed) file even after logrotate moves it. Sending USR1 (or reloading the service) tells the app to close and reopen its log file handle, so it starts writing to the new empty log.
Recovering disk space after a runaway logger fills /var/log/journal.
You get a disk-full alert. df -h shows / at 99%. Investigation:
# Find what is consuming space:
du -sh /var/log/journal/
# 4.2G /var/log/journal/
# Check the breakdown:
journalctl --disk-usage
# Archived Journals: 3.8G
# Active Journals: 412.0M
# Total: 4.2G
# The service that caused the storm (check who logged the most recently):
journalctl --since "24 hours ago" | awk '{print $5}' | sort | uniq -c | sort -rn | head
# 48291 myapp[12345]:
# 3201 kernel:
# 812 sshd[...]:
# myapp logged 48k lines in 24h — likely a debug logging left on in prod
# Immediate relief: vacuum to 500M:
sudo journalctl --vacuum-size=500M
# Vacuuming done, freed 3.7G of archived journals
# Prevent recurrence: set a cap:
sudo tee /etc/systemd/journald.conf.d/size.conf <<'EOF'
[Journal]
Storage=persistent
SystemMaxUse=1G
SystemKeepFree=500M
EOF
sudo systemctl restart systemd-journald
# Fix the root cause: turn off debug logging in myapp config▸Common mistake
Forgetting delaycompress in logrotate breaks apps that hold file descriptors open. If you compress the most recently rotated file immediately, and the app is still writing to it (via an open file descriptor to the now-renamed file), you can corrupt the compressed output. delaycompress leaves the most recent rotated file uncompressed for one cycle, giving the app time to reopen. The postrotate signal is a belt-and-suspenders measure — the signal tells the app to reopen, so it switches to the new file immediately rather than waiting for the FD to be closed naturally.
A colleague says: 'I set Storage=persistent and now the journal grows without bound — after 3 months it is 8 GB.' What did they forget, and what is the immediate fix?
journald defaults to volatile storage (/run/log/journal, wiped on reboot) unless /var/log/journal/ exists or Storage=persistent is explicitly set. Enabling persistence requires one config drop-in and systemctl restart systemd-journald. SystemMaxUse caps how much disk the journal may consume — without it, a persistent journal grows until the disk is full. journalctl --vacuum-size and --vacuum-time trim archived journals on demand without touching the active one. For plain-text app logs in /var/log, logrotate handles rotation, compression, and the postrotate signal that tells apps to reopen their file handles. The two systems are independent: journald owns its binary store; logrotate owns /var/log/*.log files.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.