# logrotate for Custom Daemons

LLMS index: [llms.txt](/en/llms.txt)

---

Log rotation is missing for your daemon, the log file has grown to dozens of gigabytes, the disk is full, and monitoring is screaming. systemd-journald and syslog-ng rotate on their own, but if your custom daemon writes directly to a file, rotation falls to logrotate. Here is how to configure it for a specific service.

## Why write a custom logrotate config

Packages from the repository usually drop their config into `/etc/logrotate.d/`, but for self-built daemons or those compiled from source, there is none. Without a config the file grows without limits. logrotate runs via a systemd timer (`logrotate.timer`) or cron and reads all files from `/etc/logrotate.d/`. Creating a single file is enough to start the rotation cycle.

> [!NOTE]
> Verify that the `logrotate` package is installed and the timer is active: `systemctl status logrotate.timer`. On most distributions it is enabled by default.

## copytruncate vs create — when to use which

These are two fundamentally different approaches to renaming and creating a new file.

| | `copytruncate` | `create` |
|---|---|---|
| **Mechanism** | Copies the current file, truncates the original in place | Renames the old file, creates a new one with correct permissions |
| **Application impact** | No restart needed | Daemon must be able to open the new file (typically via SIGHUP) |
| **Risk of lost lines** | Yes — entries written between copy and truncate can fall into the gap | Minimal — renaming is atomic |
| **When to use** | Daemon cannot re-open its file (e.g., written in Go without signal handling) | Daemon supports SIGHUP or `systemd-notify` |

> [!WARNING]
> `copytruncate` is a compromise. Lines written between `cp` and `truncate` are lost. For high-traffic services this can mean hundreds of lost lines per second.

If the daemon can receive signals, use `create` and restart it via `postrotate`.

## delaycompress and how it affects the archive chain

By default logrotate compresses the rotated file in the same cycle. The problem: if the daemon is still writing to the old file (or has not yet re-opened the new one), compression breaks everything.

`delaycompress` defers compression by one cycle. The chain looks like this:

```
app.log          ← current
app.log.1        ← rotated, not yet compressed
app.log.2.gz     ← compressed, two cycles ago
app.log.3.gz     ← compressed, three cycles ago
```

Without `delaycompress` the transition is harsher: `app.log` immediately becomes `app.log.1.gz`, and if the daemon is still writing to `app.log` through its descriptor, data goes into the compressed archive — or is lost entirely.

> [!TIP]
> `delaycompress` only makes sense together with `create` and `compress`. With `copytruncate` it works but loses its meaning — the file is truncated in place, so compression can happen immediately.

## Integration with systemd notify

If the daemon supports `sd_notify(3)`, you do not need to rely on `postrotate` with a manual restart. systemd can restart the service based on a signal from logrotate.

In the logrotate config, specify:

```
postrotate
    systemctl kill -s HUP my-daemon.service
endscript
```

Or, if the daemon listens on `NOTIFY_SOCKET`:

```
postrotate
    systemctl notify-reload my-daemon.service
endscript
```

> [!NOTE]
> `systemctl notify-reload` is available starting from systemd 231. It sends `RELOADING=1` followed by `READY=1` — the standard mechanism for notifying a configuration reload.

For `copytruncate`, `postrotate` is usually unnecessary — truncating the file in place does not require daemon involvement.

## Example working config

Suppose daemon `my-app` writes to `/var/log/my-app/app.log` and supports SIGHUP and `sd_notify`.

```
/var/log/my-app/app.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 myapp myapp
    postrotate
        systemctl notify-reload my-app.service >/dev/null 2>&1 || true
    endscript
}
```

Flags:

| Flag | What it does |
|---|---|
| `daily` | Rotate every day |
| `rotate 14` | Keep 14 archives |
| `compress` | gzip compression |
| `delaycompress` | Delay compression by one cycle |
| `missingok` | Do not error if the file is absent |
| `notifempty` | Do not rotate an empty file |
| `create 0640 myapp myapp` | Create new file with correct permissions and owner |
| `postrotate` | Notify systemd of reload |

To test without actually rotating:

```
logrotate -d /etc/logrotate.d/my-app
```

The `-d` flag runs in debug mode — it shows what would be done, without modifying any files.

> [!WARNING]
> After creating the config, the first rotation happens on the next timer tick. To force an immediate check: `logrotate -f /etc/logrotate.d/my-app`. This rotates the file right away, so use it carefully on production.
