logrotate for Application Logs That systemd Doesn’t Manage

Plenty of applications still write their own log files, straight to disk, with no idea that journald exists. Left alone, one of them will eventually fill a partition and take a production service down at three in the morning — and the fix is a fifteen-line file you should have written on day one.

If an application logs through the systemd journal or to syslog, rotation is handled for you. But a great deal of software does not. Legacy Java services, older PHP applications, vendor binaries, anything that opens /var/log/myapp/app.log and writes to it directly — none of that is managed by systemd-journald, and none of it will clean up after itself. That is what logrotate is for, and getting the rule right is mostly about one decision the tutorials gloss over.

The rule itself

On Ubuntu 24.04, logrotate is driven by /etc/logrotate.conf, which includes everything in /etc/logrotate.d/. Package-managed rotation rules live there, and so should yours. Create /etc/logrotate.d/myapp with a rule scoped to that one application. Here is a complete, working example for a service that writes its own file and can be told to reopen it on a signal.

Free · 4 minutes

Is your engineering team shipping safely, or quietly accumulating risk?

Fourteen questions on how work gets from idea to production — cadence, testing, rollback, and the key-person risk in your delivery. Banded finding on screen, full sheet by email.

# /etc/logrotate.d/myapp
# Rotation for a legacy app that writes /var/log/myapp/*.log directly.

/var/log/myapp/*.log {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    dateext
    dateformat -%Y-%m-%d
    create 0640 myapp myapp
    sharedscripts
    postrotate
        # Tell the service to reopen its log files.
        systemctl kill -s HUP myapp.service 2>/dev/null || true
    endscript
}

Every directive earns its place. daily sets the cadence; rotate 14 keeps fourteen rotations and deletes the fifteenth, so retention is a fortnight, not forever. missingok stops logrotate erroring when the glob matches nothing — useful before the app has run. notifempty skips rotation of an empty file. compress gzips old logs; delaycompress leaves the most recent rotation uncompressed for one cycle, which matters if the application, or a tail-based shipper, is still holding the previous file open.

dateext is worth calling out. Without it, logrotate uses a numeric suffix — app.log.1, app.log.2 — and every rotation renames the whole chain, so a given day’s logs keep moving. With dateext and a dateformat, each rotation gets a stable, date-stamped name like app.log-2026-07-22.gz that never changes again. When you are reconstructing an incident, a filename that pins the date is far easier to reason about, and it plays better with anything that indexes by filename.

The decision that actually matters: copytruncate or postrotate

Here is the bug that catches everyone. On Linux, an open file is referenced by its inode, not its name. When logrotate renames app.log to app.log-2026-07-22, an application that already has the file open keeps writing to the same inode — now under the new name. Your fresh app.log stays empty, and the old, supposedly rotated file keeps growing. You have rotated nothing; you have just renamed the live file and confused yourself.

There are two correct ways out of this, and choosing between them is the real content of the rule.

Option one — postrotate reload. If the application can be told to close and reopen its log files, this is the clean answer. Many daemons reopen on SIGHUP; that is what the postrotate block above does. logrotate renames the file, then signals the process, which opens a new file at the original path. No data is lost and there is no copy. The sharedscripts directive ensures the reload runs once per rule rather than once per matched file, which matters when the glob catches several logs. The catch: you must actually confirm your application reopens on that signal. Not every process does, and some use a different mechanism entirely — check the vendor documentation before you trust it.

Option two — copytruncate. If the application cannot be signalled and simply holds the file open forever, use copytruncate instead of a postrotate block. logrotate copies the current file to the rotated name, then truncates the original in place to zero length. The inode is preserved, so the application keeps writing to the same open handle — now at the top of an empty file. It works without touching the process, which is why it exists.

The trade-off is a small race: anything written between the copy and the truncate is lost, and for a brief moment the file exists twice on disk, so you need headroom for a second copy. Use copytruncate when you have no other option, and do not combine it with a create directive — with copytruncate the original file is never removed, so create has nothing to do. A copytruncate variant of the rule drops the create and postrotate lines and adds a single copytruncate directive.

Test it before you trust it

Never deploy a rotation rule unverified. logrotate has a debug mode that reads your configuration and tells you exactly what it would do without changing anything on disk or updating its state file.

# Dry run: parse the rule and print the plan, change nothing.
# -d implies verbose and forces debug (no state written).
sudo logrotate -d /etc/logrotate.d/myapp

# Force a real rotation now, ignoring the schedule, to prove it works.
sudo logrotate -f /etc/logrotate.d/myapp

# Then confirm the app is still writing to the live file:
sudo ls -li /var/log/myapp/
sudo lsof /var/log/myapp/app.log

Read the -d output line by line: it names each file it considers, says whether the rotation is due, and prints the scripts it would run. The -f flag then forces a genuine rotation immediately, regardless of the daily schedule — the fastest way to prove the whole cycle, signal and all, actually works. After forcing, the ls -li inode number on the live file should differ from the rotated one under a postrotate reload, and lsof should show the process holding the new file, not the archived one. That single check is the difference between a rule that rotates and a rule that silently does nothing.

When it runs, and why it sometimes doesn’t

On Ubuntu 24.04 logrotate is triggered by a systemd timer, logrotate.timer, which fires logrotate.service once a day — not by the old cron entry some guides still assume. Check it with systemctl list-timers logrotate.timer. This matters for a subtle reason: a daily rule does not rotate at a fixed clock time, it rotates the first time logrotate runs on a new calendar day. If the timer is disabled, or the machine is asleep at the scheduled hour, rotation simply does not happen, and the state file at /var/lib/logrotate/status records when each log was last rotated so the next run can catch up.

That is also why disk-fill incidents cluster on machines that are busy exactly when everyone is asleep. A log that grows fastest overnight, on a host whose logrotate timer never fires because the service is masked or the box is under a suspend policy, is the classic slow-motion outage. If you track it, a rising node_filesystem_avail_bytes alert on the log partition is the honest early warning — the same instinct behind instrumenting the metrics that actually predict failure rather than the ones that look good on a dashboard.

Rotation is unglamorous plumbing, which is exactly why it gets skipped and exactly why it takes services down. Treat the rule as configuration that ships with the application, keep it under version control alongside your other baseline config — the same discipline as a pre-commit baseline that actually runs — and remember that logs often hold secrets and personal data, so the archived .gz files inherit the same handling obligations as the credentials you take care to hash and rotate. A fifteen-line file, tested once with -d and -f, buys you back a whole class of 3am incident.

Build and rescue work

Hands-on delivery of this kind is handled by Sixteen Pillars Studio.

Free interactive tool

Website compliance checklist

What your site has to do, based on what it actually does

Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.

Most technology problems are not technology problems. They are control problems.

The systems exist. The investment has been made. The question is whether leadership can understand, direct, evidence, and sustain what those systems produce. Find out where control exists — and where it only appears to.