> ## Content Index
> Fetch the complete content index at: https://blog.sudowl.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Linux Log Rotation with logrotate — Before Your Disk Fills Up
- URL: https://blog.sudowl.com/logrotate-linux-log-rotation/
- Published: 2026-04-01T10:00:00.000Z
- Updated: 2026-10-11T08:47:37.000Z
- Description: logrotate for your own apps on Ubuntu/Debian: a working config, create vs copytruncate, postrotate reloads, maxsize, testing with -d, common traps.
- Author: Lukas
- Tags: Linux, Debian, Ubuntu

Distro packages ship their own logrotate configs, so `/var/log/syslog` and nginx logs rotate without you doing anything. The problem is everything *you* install: the app in `/opt`, the game server writing `server.log` into its own directory, the cron script appending to `/var/log/whatever.log`. Nobody rotates those, and one day the disk is full of a single 30 GB text file.

**TL;DR:** one file per app in `/etc/logrotate.d/`, use `create` \+ a `postrotate` reload if the app can reopen its log, `copytruncate` if it can't, test with `logrotate -d`, and remember logrotate only runs once a day by default.

```bash
sudo tee /etc/logrotate.d/myapp >/dev/null <<'EOF'
/var/log/myapp/*.log {
    daily
    rotate 14
    maxsize 200M
    missingok
    notifempty
    compress
    delaycompress
    dateext
    create 0640 myapp adm
    sharedscripts
    postrotate
        systemctl kill -s HUP myapp.service >/dev/null 2>&1 || true
    endscript
}
EOF

sudo logrotate -d /etc/logrotate.d/myapp     # dry run, explains what it would do
sudo logrotate -vf /etc/logrotate.d/myapp    # force a real rotation once

```

## How it actually runs

logrotate isn't a daemon. On Ubuntu 24.04 and Debian 12/13 it's started by a systemd timer, once a day:

```bash
systemctl list-timers logrotate.timer
systemctl cat logrotate.service

```

There's still a `/etc/cron.daily/logrotate` script on some installs, but it exits early when systemd is running — the timer is what counts. Each run reads `/etc/logrotate.conf`, which includes everything in `/etc/logrotate.d/`, and checks each log against its rules. When a log was last rotated is tracked in `/var/lib/logrotate/status`.

That last part matters: `daily` doesn't mean "at midnight", it means "if the last rotation of this file was at least a day ago when the timer fires".

## The config, line by line

- `/var/log/myapp/*.log` — which files. Use `*.log`, not `*` — see the gotcha below.
- `daily` / `weekly` / `monthly` — rotation interval.
- `rotate 14` — keep 14 old files, delete older ones. With `daily` that's two weeks.
- `maxsize 200M` — rotate early if the file is bigger than this, even if the interval hasn't passed. Only checked when logrotate runs, though.
- `missingok` — don't error if the log doesn't exist (yet).
- `notifempty` — don't rotate empty files.
- `compress` \+ `delaycompress` — gzip old logs, but leave the most recent rotated one (`.1`) uncompressed. Useful when the app might still write a few lines to the old file right after rotation, and handy for `less` without `zless`.
- `dateext` — rotated files get a date suffix (`app.log-20260401`) instead of `.1`, `.2`... I like this because file names don't shift every day and backup tools don't re-copy everything.
- `create 0640 myapp adm` — after moving the old log away, create a fresh empty one with these permissions and owner.
- `sharedscripts` — run `postrotate` once for the whole glob, not once per matched file.
- `postrotate ... endscript` — tell the app to reopen its log file.

## create vs copytruncate

This is the one decision that matters, and it's about how the app holds its log file.

**`create` (default):** logrotate renames `app.log` to `app.log-20260401` and creates a new empty `app.log`. But the running app still has the *old* file open — it keeps writing into the renamed file, because a file handle points at the inode, not the name. That's why you need `postrotate` to make the app reopen its log: `SIGHUP`, `SIGUSR1`, a reload command, whatever the app supports. nginx uses `USR1` (its shipped config does `invoke-rc.d nginx rotate`), many daemons reopen logs on `HUP`. Check your app's docs — sending `HUP` to something that interprets it as "terminate" is a bad morning.

**`copytruncate`:** logrotate copies `app.log` to the rotated name, then truncates the original to zero. The app keeps writing to the same inode and never notices. Use it for apps that can't reopen logs: most Java game servers, random binaries, scripts with `>> file.log`.

The price of `copytruncate`:

- Lines written between the copy and the truncate are lost. Usually a few milliseconds' worth, but it's not zero.
- The file is written twice during rotation, which hurts with multi-GB logs.

If you're writing your own tool that tails logs, this is also why you watch the inode: with `create` the inode changes and you have to reopen; with `copytruncate` it stays the same but the size suddenly drops. The [Discord alert bot](/discord-alerts-server-logs-systemd/) post deals with exactly that.

## Gotcha: copytruncate and apps that don't append

If the app opened its log *without* `O_APPEND` (plain "open for writing" and remember the offset), `copytruncate` truncates the file to 0, and the app's next write lands at its old offset — say 4 GB in. The file is now "4 GB" again, but it's a sparse file full of null bytes. `ls -lh` shows the full size, `du -h` shows almost nothing, and `less` shows a wall of `^@`.

Check with:

```bash
ls -lh /var/log/myapp/app.log
du -h  /var/log/myapp/app.log

```

If they differ wildly after a rotation, that's it. Fix: make the app open in append mode (for shell redirects: `>>` not `>`), or switch to `create` with a reload, or have systemd capture the output into the journal instead.

## Gotcha: the glob catches already-rotated files

```
/var/log/myapp/* {

```

matches `app.log`, but also `app.log-20260401.gz`, which then gets rotated *again* into `app.log-20260401.gz-20260402.gz`, and so on. Always end the pattern with the actual log extension: `*.log`.

## Gotcha: "insecure permissions" and skipped configs

Two errors that make logrotate silently skip your config (you'll only see them in `journalctl -u logrotate`):

```
error: skipping "/opt/game/logs/server.log" because parent directory has insecure permissions
(It's world writable or writable by group which is not "root")

```

The log directory is writable by a non-root group, and logrotate refuses to work there as root (symlink attacks). Fix with the `su` directive so it runs as the owner:

```
/opt/game/logs/*.log {
    su game game
    ...
}

```

The other one: the config file in `/etc/logrotate.d/` itself must be owned by root and not group/world writable, otherwise it's ignored with a "bad file mode" message. If you created it with an editor running as another user, `sudo chown root:root` and `chmod 644` it.

## Gotcha: size-based rotation needs more frequent runs

`size 100M` and `maxsize 100M` are only evaluated when logrotate runs. With the default daily timer, a log that grows 50 GB per day will still reach 50 GB. For chatty logs, run logrotate hourly:

```bash
sudo systemctl edit logrotate.timer

```

```ini
[Timer]
OnCalendar=
OnCalendar=hourly

```

The empty `OnCalendar=` clears the default before setting the new one — without it you get both. Then `daily` rules still rotate once a day (the status file tracks that), but `maxsize` gets checked every hour. If you're new to timer overrides, [systemd timers](/systemd-timers-instead-of-cron/) explains how they work.

## Testing without breaking things

```bash
# dry run: parses config, shows what would happen, changes nothing
sudo logrotate -d /etc/logrotate.d/myapp

# force rotation now, verbose
sudo logrotate -vf /etc/logrotate.d/myapp

# check the full config parses (all files)
sudo logrotate -d /etc/logrotate.conf 2>&1 | grep -i error

```

`-d` implies no changes, so it's safe on production. Note that `-f` on a single file still uses the global state file, so forcing it once is harmless — it just records a rotation.

## What logrotate doesn't handle

- **The systemd journal.** journald rotates itself; limit it with `SystemMaxUse=` in `journald.conf` (details in [disk-full forensics](/linux-disk-full-forensics/)).
- **Docker container logs.** Docker owns those files; set `max-size`/`max-file` in `daemon.json` — see [installing Docker](/docker-install-ubuntu/).
- **Apps that rotate themselves.** Many Java apps (log4j, logback) roll their own files. Don't add logrotate on top — two rotators fighting over the same file is a mess. Configure retention in the app instead.

For anything else that writes plain text files: one small config in `/etc/logrotate.d/`, run `-d` once, done.

---

### Related posts

- [Linux User Management — adduser, Groups, sudo and SSH Keys](https://blog.diengdoh.com/linux-user-management-adduser-sudo-ssh-keys/?ref=blog.sudowl.com)
- [Set Up Automatic Security Updates on Ubuntu and Debian](https://blog.diengdoh.com/ubuntu-automatic-updates-unattended-upgrades/?ref=blog.sudowl.com)
- [Proxmox + Cloud-Init — Spinning Up an Ubuntu VM in 30 Seconds](https://blog.diengdoh.com/proxmox-cloud-init-vm-template-ubuntu/?ref=blog.sudowl.com)