logrotate: Tutorial & Best Practices

The quiet janitor that keeps your logs from eating the disk, one rename at a time.

What It Is

Here is a puzzle every new admin eventually trips over. Your server has been happily writing logs for months — every request, every error, every login attempt — and the disk hasn't filled up. Yet nobody deletes those logs by hand. So who's doing it? Something must be, because a busy nginx or mysql can write gigabytes a week, and a disk full on /var/log is one of the most common ways a Linux box quietly falls over.

The answer is logrotate, and it's so unobtrusive that most people who run servers for years never consciously meet it. It's a tiny program with exactly one job: take a log file that's getting big, rename it out of the way, start a fresh empty one in its place, eventually compress the old ones, and after a while throw the oldest away entirely. It runs once a day, does its work in a few hundred milliseconds, and goes back to sleep. That's the whole show — and it's the reason your disk never silently fills with yesterday's chatter.

If you've never run a server, this is a lovely first thing to understand, because it teaches you something deeper than logrotate itself: on a real server, almost everything important happens on a schedule you didn't set up, run by a program you've never seen. Pull this one thread and you'll meet cron, systemd timers, file descriptors, and the difference between a file's name and its contents — a distinction that sits at the heart of how Unix actually works. By the end of this page you'll not only configure logrotate like a pro, you'll understand the one gotcha that silently eats log lines on thousands of servers, and you'll know exactly which side of it you're on.

Your First Look

logrotate isn't a thing you run so much as a thing that runs you — it lives in config files. The master one is /etc/logrotate.conf, and on a stock Debian/Ubuntu box it's refreshingly short:

# rotate log files weekly
weekly

# keep 4 weeks worth of backlogs
rotate 4

# create new (empty) log files after rotating old ones
create

# uncomment this if you want your log files compressed
#compress

# packages drop log rotation information into this directory
include /etc/logrotate.d

Read that top to bottom and you already understand 80% of logrotate. Rotate logs weekly. Keep 4 old copies. After rotating, create a fresh empty file. Those three lines are global defaults — they apply to every log unless a more specific rule overrides them. The last line, include /etc/logrotate.d, is the clever bit: it tells logrotate to also read every file in that directory, which is where each installed package drops its own rotation rules. Peek inside:

$ ls /etc/logrotate.d/
alternatives  apt  bootlog  btmp  dpkg  mysql-server  ppp  ...

When you apt install nginx, the package quietly leaves a nginx file in there saying "here's how I'd like my logs rotated." logrotate finds it next time it runs. Nobody had to wire anything together — that's the convention-over-configuration beauty of the Linux log ecosystem, and it's why a freshly installed service "just handles its logs" without you lifting a finger.

And the result of all this, sitting right there in /var/log/apt/, the rotated files themselves:

$ ls -la /var/log/apt/
-rw-r-----  1 root adm    7349 term.log          # today's, being written
-rw-r-----  1 root adm  110629 term.log.1.gz     # last month's, compressed

term.log is live. term.log.1.gz is the previous period's, renamed with a .1 and squeezed down with gzip. That .1 is the whole mechanism in one character — let's see how it gets there.

How It Works: The Rename Dance

Here's the mental model, and it's wonderfully simple once you see it. logrotate doesn't "trim" a log file — it shuffles names down a line, like a bucket brigade.

Say you've got app.log and you keep 3 rotations. Each time logrotate decides it's time, it walks the line from the oldest backward:

app.log.3   →  deleted (it's aged out)
app.log.2   →  becomes app.log.3
app.log.1   →  becomes app.log.2
app.log     →  becomes app.log.1
(then a fresh empty app.log is created)

Every rotation, the whole convoy shifts one step toward the exit. app.log.1 is always yesterday-ish, app.log.3 is the oldest you're keeping, and anything that would've become .4 falls off the end into oblivion. That rotate 3 number is, very literally, how far back in time you can see. Bump it to rotate 30 and you keep a month; set it to rotate 0 and old logs are deleted instead of kept at all.

The genuinely important question — and the one most tutorials breeze past — is what happens to the application during this dance. Your web server had app.log open and was happily writing to it. logrotate just renamed that file to app.log.1. Does the web server notice? Does it follow the rename? Does it start writing to the new empty file?

No. And understanding why is the single most important thing on this page.

The One Thing Every Other Tutorial Leaves Muddy

Here's the crux, taught once and properly. When a program opens a log file, the kernel hands it back a file descriptor — a little numbered handle. And the crucial, slightly mind-bending truth is this: that handle points at the file's contents (technically its inode), not at its name. The name is just a label in a directory; the data lives somewhere else entirely, and the descriptor is wired straight to the data.

So when logrotate renames app.log to app.log.1, it changes the label — but your web server is still holding a descriptor pointed at the same underlying data. It keeps writing, blissfully unaware, and its log lines now land in the file that's called app.log.1. Meanwhile the shiny new empty app.log that logrotate created sits there, receiving nothing. Your "current" log is empty and your writes are going into the "old" one. That's not a bug — it's exactly how Unix file handles work — but it's a trap with two completely different solutions, and choosing the wrong one silently loses log lines.

Note

A renamed-but-still-open file is the same mechanism behind a classic mystery: you rm a huge log, df still shows the disk full, and the space only frees when you restart the service. The file's name is gone, but a process still holds an open descriptor to its contents, so the kernel keeps the data alive until that descriptor closes. Name and data are different things — once that clicks, half of Linux makes more sense.

Solution one: tell the app to reopen (create + a signal)

The clean fix: after the rename, tell the program to let go of the old file and open the name again. Almost every well-behaved daemon reopens its log files when you send it a signal (usually SIGHUP, sometimes SIGUSR1). logrotate does this with a postrotate script:

/var/log/nginx/*.log {
    weekly
    rotate 14
    compress
    create 640 nginx adm
    sharedscripts
    postrotate
        kill -USR1 $(cat /run/nginx.pid)
    endscript
}

The sequence: logrotate renames access.logaccess.log.1, creates a fresh empty access.log owned by nginx:adm with mode 640, then runs the postrotate script which pokes nginx with a signal. nginx closes its old descriptor, opens access.log afresh — and now its writes land in the new file. Not one log line lost. This is the gold-standard method, and it's why create and postrotate so often appear together.

Solution two: copytruncate (and the lines it quietly eats)

But what about a program that can't be told to reopen — some app you didn't write, with no signal handler, that just holds the file open forever? For that there's copytruncate, and here is its exact mechanism:

  1. Copy the live app.log to app.log.1.
  2. Truncate the original app.log back to zero bytes in place — same inode, same descriptor, so the app keeps writing to it none the wiser.

Clever — no signal needed, no cooperation from the app. But read the man page's own warning, because it's honest about the cost:

Note that there is a very small time slice between copying the file and truncating it, so some logging data might be lost.

That's the gotcha. Between the copy finishing and the truncate landing, the app might write a few more lines — and those lines get truncated away before they're ever copied. On a quiet log, never noticed. On a firehose, you lose a sliver every single rotation. copytruncate trades a guarantee for convenience: it always works, but it's lossy by design.

Warning

The copytruncate vs create+signal choice is the logrotate decision that bites people. If your app can reopen on a signal, use create + postrotate — it loses nothing. Reach for copytruncate only when the app genuinely can't be told to let go of the file. Picking copytruncate out of habit means quietly dropping log lines forever, and you won't get an error to tell you.

Who Pulls The Trigger: cron, then systemd

logrotate has no daemon of its own. It isn't sitting in memory watching your files — it runs, does its work, and exits. So something has to wake it up, and "once a day" is the convention. Historically that something was cron: a script in /etc/cron.daily/logrotate that simply runs logrotate /etc/logrotate.conf. On any modern systemd box, though, that cron script now starts with a polite little hand-off:

#!/bin/sh
# skip in favour of systemd timer
if [ -d /run/systemd/system ]; then
    exit 0
fi
...

If systemd is running the show, the old cron job bows out, and a systemd timer takes over instead. You can see it scheduled right now:

$ systemctl list-timers | grep logrotate
Thu 2026-06-04 00:13:48   29min   logrotate.timer  logrotate.service

The timer unit itself is worth a look, because it hides two genuinely smart features:

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true

OnCalendar=daily is the "midnight-ish" schedule. But RandomizedDelaySec=1h smears the actual run across a random hour after midnight — so on a fleet of fifty servers they don't all hit the disk and CPU at 00:00:00 in a thundering herd. And Persistent=true is the lovely one: if the machine was asleep or powered off when the timer should've fired, systemd runs it as soon as the box wakes up. A laptop that was shut for the weekend still gets its logs rotated Monday morning. (Plain cron couldn't do that — a missed cron job is simply missed; this is one of the quiet reasons systemd timers won.)

Pro Tip

Want to know if logrotate ran and when each file was last rotated? It keeps a diary at /var/lib/logrotate/status — one line per file with the date it last touched it. If a log is mysteriously huge, cat that file: a stale or missing date tells you logrotate never got to it, usually because of a config error you'll catch with logrotate -d (below).

How I'm Using It

When I add a service that logs somewhere logrotate doesn't already know about, the routine is short and the same every time.

1. Drop a file in /etc/logrotate.d/. Never edit logrotate.conf itself for app logs — put each app's rules in its own file. I name it after the service so future-me finds it instantly:

/var/log/myapp/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 640 myapp adm
    sharedscripts
    postrotate
        systemctl reload myapp >/dev/null 2>&1 || true
    endscript
}

2. Dry-run it before trusting it. This is the step that separates a calm admin from a surprised one. logrotate -d (debug) reads your config and tells you exactly what it would do — every rename, every compress, every delete — without touching a single file:

logrotate -d /etc/logrotate.d/myapp

Read that output like a flight plan. It'll say "rotating log /var/log/myapp/app.log, log needs rotating" or, just as usefully, "log does not need rotating" so you learn nothing's broken — it's simply not time yet.

3. Force one real rotation to confirm. Once the dry run looks right, I prove it end-to-end with -f (force), which rotates now regardless of schedule:

logrotate -vf /etc/logrotate.d/myapp

-v (verbose) narrates each step; -f ignores the "it's not due yet" logic. After it runs I check that a .1 appeared, that the app is still writing to the new file (ls -la — the live one should be growing again), and that ownership matches my create line. If all three are true, I'm done and I never think about that log again.

That's the whole craft: own file in logrotate.d, -d to preview, -vf to prove it, then walk away.

The Directives, Explained

logrotate's config language is small, and these are the directives worth knowing — global ones set defaults, per-block ones override them inside { ... }.

Frequency (how often to rotate):

  • hourly / daily / weekly [weekday] / monthly / yearly — time-based. weekly 0 is Sunday, weekly 7 means "every 7 days regardless of weekday." Remember the trigger only fires as often as logrotate is run, so hourly does nothing unless you also run logrotate hourly.
  • size 100M — rotate when the file exceeds this size, ignoring the clock entirely. Suffixes k, M, G.
  • minsize 100M — rotate on the schedule, but only if the file has grown past this size. Tiny logs get skipped.
  • maxsize 100M — rotate on the schedule or sooner if it blows past this size. The belt-and-braces option: time-based, but with a hard ceiling so a sudden flood can't fill the disk before midnight.

How many to keep, and aging:

  • rotate count — keep count old copies before deleting. rotate 0 deletes immediately; rotate -1 never deletes (dangerous — that's how disks fill).
  • maxage count — delete rotated logs older than count days, regardless of how many there are.
  • minage count — don't rotate a log younger than count days.
  • start count — the number rotation begins at (default 1, giving .1, .2, …).

Creating the replacement:

  • create mode owner group — make a fresh empty log after rotating, with these permissions. Omit any field to inherit it from the old file. This is the partner to a postrotate signal.
  • nocreate — don't create a replacement (for copytruncate, where the original stays in place).
  • copy / copytruncate / renamecopy — the "app holds the file open" family. copy snapshots without touching the original; copytruncate copies then zeroes in place (lossy — see above); renamecopy renames-then-copies, handy for moving rotated logs to another device.
  • su user group — rotate as this user/group instead of root; the safe way to handle logs in directories owned by non-root users.

Compression:

  • compress — gzip the rotated copies. A text log compresses ~10–20×, so this is almost free disk space.
  • delaycompress — wait one extra cycle before compressing, so .1 stays uncompressed and .2+ are .gz. Why bother? Because a program that reopens its log a moment after rotation might still scribble a last line into .1 — compressing it instantly would corrupt that. Delaying one round leaves the most-recent rotation as plain text, both safe and easy to tail. You see this in the wild constantly:
/var/log/dpkg.log {
    monthly
    rotate 12
    compress
    delaycompress      # so dpkg.log.1 is plain text, .2.gz onward compressed
    missingok
    notifempty
    create 644 root root
}
  • compresscmd / compressext / compressoptions — swap gzip for xz/zstd, change the extension, pass flags. Default is gzip -6.

Safety and edge cases:

  • missingok — if the file doesn't exist, skip it silently instead of erroring. Almost always what you want for wildcard patterns.
  • notifempty — don't rotate an empty log (pairs nicely with missingok). ifempty is the opposite and the default.
  • dateext — name rotated files with a date (app.log-20260603) instead of .1, .2. Easier to find a specific day; tune the format with dateformat. One subtlety: the date must be lexically sortable (year-month-day), because logrotate sorts filenames to decide what's oldest.
  • olddir directory — move rotated logs into a separate directory to keep the main one tidy. createolddir makes it if missing.
  • shred — overwrite old logs with shred before deleting, so deleted logs can't be recovered. For logs with secrets in them.

Scripts (run around the rotation):

  • prerotate / postrotate — shell snippets (ending in endscript) run before and after rotation. postrotate is where the "signal the app to reopen" magic lives.
  • firstaction / lastaction — run once, before/after the entire batch of matching files (vs postrotate, which can run per-file).
  • preremove — runs just before a log is finally deleted.
  • sharedscripts — run the scripts once for the whole wildcard group, not once per matched file. Crucial when a single kill -HUP reopens all of a service's logs — you don't want to HUP it five times.

Reading It by Example

Build instinct by reading real config blocks the way an admin does — the directives tell you the intent.

The package-manager pattern (apt):

/var/log/apt/history.log {
    rotate 12
    monthly
    compress
    missingok
    notifempty
}

Twelve monthly rotations = a full year of history, compressed, and don't fuss if the file's absent. Notice there's no create and no postrotate — apt isn't a long-running daemon holding the file open; it opens, writes, closes. So a plain rename is perfectly safe and nobody needs signalling. The absence of create/postrotate tells you "this writer doesn't hold the file open."

The daemon-with-signal pattern (mysql-server):

/var/log/mysql/mysql.log /var/log/mysql/mysql-slow.log /var/log/mysql/error.log {
    daily
    rotate 7
    missingok
    create 640 mysql adm
    compress
    sharedscripts
    postrotate
        test -x /usr/bin/mysqladmin || exit 0
        ... mysqladmin ... flush-error-log flush-slow-log
    endscript
}

Now read the story: multiple files in one block; create 640 mysql adm makes fresh empty ones with the right owner; sharedscripts so the postrotate runs once for the whole group; and that postrotate calls mysqladmin ... flush-*-logMySQL's way of saying "close your old log handles and reopen." This is solution one in the wild: rename, recreate, then tell the daemon to let go. Zero lost lines. When you see create + sharedscripts + postrotate, you're looking at a service that holds its logs open and gets signalled to reopen them.

The delaycompress tell: any block with both compress and delaycompress (like dpkg above) is saying "I want compression, but leave the freshest rotation readable as plain text" — usually because a writer might add one last line right after rotation, or just so a human can tail app.log.1 without un-gzipping it.

Gotchas

  • copytruncate loses lines. Covered above, but it's the big one. Default to create + a postrotate signal; use copytruncate only when the app truly can't reopen.
  • logrotate only runs as often as it's invoked. Putting hourly in a config does nothing if the systemd timer fires daily — logrotate "won't modify a log more than once a day" unless triggered by size or forced with -f. To rotate hourly you must run logrotate hourly.
  • A bad config block stops the rest. A syntax error or a failing postrotate script can abort the whole run, leaving other logs un-rotated and the disk creeping up. logrotate -d catches this before it bites.
  • Permissions on config files matter. logrotate flatly refuses to read a config file in /etc/logrotate.d/ that's group- or world-writable — a security guard, since these files run shell scripts as root. If a rule seems ignored, check ls -l on the file.
  • rotate -1 plus a chatty log = full disk. "Never delete" sounds safe; it's how /var/log quietly hits 100%. Always cap retention with rotate N or maxage.
  • Wildcards can rotate already-rotated files. A bare * matches app.log.1.gz too. Use a precise pattern like *.log, or an olddir, so logrotate doesn't try to re-rotate its own output. The man page warns about exactly this.
  • systemd's ProtectSystem=full. The shipped logrotate.service runs sandboxed and can't write under /etc or /usr — fine for normal logs in /var/log, but a surprise if you stash logs somewhere unusual.

History & Philosophy

logrotate was written in the mid-1990s by Erik Troan at Red Hat, and it has the unmistakable shape of a tool born from a specific, recurring annoyance: logs grow, disks are finite, and doing the cleanup by hand is exactly the kind of dull, error-prone chore computers exist to absorb. It solved that so completely that it became invisible — the highest compliment a piece of infrastructure software can earn. Nobody talks about logrotate at conferences; it just works, on essentially every Linux server alive, the way a well-hung door is something you only notice when it squeaks.

What's worth carrying away is the philosophy it quietly embodies, because you'll see it everywhere once you spot it here. logrotate is declarative: you don't write a program that rotates logs, you write a little description of what good looks like — keep 14 days, compress the old ones, signal the daemon — and a separate, dumb, reliable engine makes reality match your description, once a day, forever. That's the same idea behind systemd unit files, behind nftables rulesets, behind every "infrastructure as code" tool you'll ever touch: stop writing the steps, start describing the destination. logrotate is one of the first places a new admin meets that pattern, in miniature, where it's small enough to hold whole in your head.

And there's a genuinely lovely connection lurking under the whole thing — the same one from the muddle section, now turned into wisdom. The entire reason copytruncate has to exist is that a file's name and a file's data are separable, and a long-running program clings to the data, not the name. That single fact — inodes versus directory entries — is the hinge logrotate swings on, and it's the same hinge behind "deleted file still using disk space," behind atomic config swaps, behind how mv is instant but cp is slow. Learn it once here and you've earned a key that opens a dozen other doors. That's the quiet gift of the humblest tools: the small one you came to understand teaches you the big one you didn't know you needed.

See Also

  • cron — the original "run this on a schedule" engine that first triggered logrotate
  • systemd — its timers run logrotate today, with randomized delay and catch-up
  • gzip — what compresses your .1 into .1.gz
  • shred — secure-delete for logs that hold secrets
  • nginx — the classic postrotate + signal customer
  • mysql — ships the sharedscripts + flush-logs config we read above
  • disk full — what happens when log rotation isn't keeping up
  • /var/log — the directory logrotate spends its whole life tidying

What happens when logrotate quietly stops and /var/log creeps toward 100%?

CleverUptime watches disk usage on every server and warns you while there's still room to act — long before a full /var/log takes the whole box down — and tells you in plain language which directory is filling up, so a stalled rotation never becomes a 3am outage.

Want to see your own server's health right now? One command, no signup, no install.

Check your server →