stop unbounded logs before they become an outage
Log files that are never rotated grow silently until a disk fills up and a production server crashes. logrotate keeps log files in check by size or time, compresses old files, and runs hooks such as service restarts after rotation. This article shows a practical configuration for a custom PHP application log and how to test it safely with debug mode before going live.
Table of Contents
- 1. Why unrotated logs become a problem
- 2. logrotate basics and configuration structure
- 3. Triggers: size based and time based rotation
- 4. Compression and retention
- 5. Prerotate and postrotate hooks
- 6. A practical configuration for a PHP application
- 7. Safely testing with logrotate --debug
- 8. Automation, the status file, and deployment
- 9. logrotate compared: common mistakes vs. correct configuration
- 10. Summary
- 11. FAQ
1. Why unrotated logs become a problem
Every PHP application that logs errors, records requests, or uses its own Monolog handler continuously writes to one or more log files. Without a limit, such a file grows unnoticed over weeks and months until it spans several gigabytes. The real problem only shows up once the underlying partition, often /var, hits 100 percent: MySQL can no longer perform write operations, PHP-FPM workers die with obscure error messages, cron jobs abort silently, and the web server suddenly starts returning 500 errors even though nothing changed in the code.
In practice this scenario happens surprisingly often, because developers think of many things when deploying a new application, but rarely of log rotation. A quick check with df -h shows partition usage, and du -sh /var/log/* /var/www/*/var/log/* reveals the biggest offenders. logrotate ships preinstalled on virtually every Linux distribution and runs daily by default via cron or a systemd timer. Anyone who does not hook custom application logs into this system is only postponing the disk space problem to a later, usually much less convenient, moment.
2. logrotate basics and configuration structure
The global configuration lives under /etc/logrotate.conf and defines default values that apply to every rule that follows, unless a rule overrides them. Application specific rules do not belong in this central file, but as separate files under /etc/logrotate.d/, which is read in automatically via an include directive. A rule consists of the path to the log file followed by a block in curly braces holding the desired directives, such as the rotation interval, the number of retained copies, and file permissions.
logrotate is not a long running daemon but a one shot invocation, typically triggered through /etc/cron.daily/logrotate or, on more modern systems, through the logrotate.timer timer unit. To avoid rotating the same log file twice on the same day, logrotate keeps a status file under /var/lib/logrotate/status recording the time of the last rotation for each log path. This separation between global configuration, modular per application files, and a status file keeps the system manageable, even when dozens of applications ship their own logs.
# /etc/logrotate.conf - global default values
weekly
rotate 4
create
dateext
compress
# Application specific rules get included from here
include /etc/logrotate.d
# Example: minimal rule block in /etc/logrotate.d/example
/var/log/example/app.log {
daily
rotate 14
compress
missingok
notifempty
}
3. Triggers: size based and time based rotation
Time based rotation with daily, weekly, or monthly is the default case and works well when log volume is reasonably predictable over time. The rotate N directive determines how many old versions are kept before the oldest one gets deleted. For audit or compliance requirements, where a defined retention period of logs must exist, time based rotation is almost always the right choice, because the retention period follows directly from rotate multiplied by the interval.
Under heavily fluctuating traffic, for example during a sale event or a DDoS attempt, a log can grow within a few hours, well before the next daily rotation kicks in. This is where size 100M helps, rotating regardless of the time interval as soon as the file exceeds the given size, or the combination of daily and maxsize 100M, which watches both criteria at once and triggers on whichever comes first. For production PHP applications with unpredictable growth patterns, this combination of time and size triggers is the most robust solution, because it offers both predictable daily rotation and an emergency mechanism against a sudden log surge.
4. Compression and retention
The compress directive compresses rotated log files with gzip by default, which cuts storage needs for typical text logs by 80 to 95 percent. An important detail here is delaycompress: without it, the most recently rotated file gets compressed immediately, even though the writing process may still briefly write to the old, already renamed file handle before closing and reopening it after the hook runs. delaycompress pushes compression to the next rotation cycle, preventing lost writes from ending up unnoticed inside an already compressed file.
For alternative compression tools, compresscmd and compressext can be set, for example to use xz instead of gzip when disk space is scarcer than CPU time. The dateext directive appends the rotation date to the file name instead of a sequential number, which makes locating old logs by hand considerably easier. Combined with dateformat -%Y%m%d, the format can be adjusted to your own conventions, for example to keep it consistent with other backup scripts.
5. Prerotate and postrotate hooks
The postrotate block contains shell commands that run immediately after rotation, and it is required in almost every production configuration. The reason lies in how logrotate works: by default the old file gets renamed and a new, empty file with the same name is created. The writing process, however, still holds the old, now renamed file handle open and happily keeps writing into the now invisible, rotated file instead of the new one. The postrotate hook tells the application to reopen its file handle, for example via systemctl reload php-fpm or a targeted signal to the process.
The prerotate block correspondingly runs before rotation and is useful for pre checks or acquiring a lock. When a configuration covers multiple glob patterns at once, sharedscripts prevents the hook from running separately for every matching file, which avoids unnecessary repeated executions and potential race conditions with a reload command. Every postrotate and prerotate block must be closed with endscript, otherwise logrotate incorrectly interprets the following lines as further shell commands.
#!/usr/bin/env bash
# Excerpt from a postrotate block: reopen the log handle after rotation
postrotate
# Reload PHP-FPM so the master process reopens the log file
if systemctl is-active --quiet php8.3-fpm; then
systemctl reload php8.3-fpm
fi
# Alternative for applications with their own signal handler
if [ -f /run/myapp/myapp.pid ]; then
kill -USR1 "$(cat /run/myapp/myapp.pid)" 2>/dev/null || true
fi
endscript
6. A practical configuration for a PHP application
A typical PHP application writes via Monolog or a custom logging class to /var/www/myapp/var/log/app.log, running under the system user www-data. For this case, missingok and notifempty are essential: missingok prevents an error if the log file happens to be missing in the meantime, for example right after a fresh deployment, and notifempty skips rotation if no new lines were written since the last run anyway. The create 0640 www-data www-data directive ensures that the new, empty log file gets created with correct permissions, instead of accidentally owned by root, which would otherwise stop the application from writing after the very first rotation.
When choosing between create and copytruncate: create combined with a correct postrotate hook is the cleaner solution, because it cannot lose data between copying and truncating the file. copytruncate copies the current content and then truncates the original file without switching the file handle, which is convenient for applications without a reload mechanism, but leaves a small window where writes between the copy and the truncate can get lost. For custom PHP applications where you control the deployment process, create plus a reload hook is therefore the recommended default.
# /etc/logrotate.d/myapp
/var/www/myapp/var/log/app.log {
daily
maxsize 100M
rotate 14
compress
delaycompress
missingok
notifempty
dateext
dateformat -%Y%m%d
create 0640 www-data www-data
sharedscripts
postrotate
systemctl reload php8.3-fpm >/dev/null 2>&1 || true
endscript
}
7. Safely testing with logrotate --debug
Never run a new logrotate configuration untested in production. Calling logrotate -d /etc/logrotate.d/myapp enables debug mode, which reads the configuration, reports syntax errors, and prints which file would be rotated under which criterion, without actually renaming, compressing, or running any hook. That makes debug mode ideal for quickly checking whether the configuration is even found and parsed correctly, before you rely on the next cron run to catch problems.
If a pure syntax check is not enough, logrotate -f /etc/logrotate.d/myapp forces a real rotation, regardless of whether the time or size criterion is met. This should first happen in a staging environment, since it actually rotates files, compresses them, and runs the hooks. After a forced run it is worth checking the permissions of the new file with ls -la /var/www/myapp/var/log/ and confirming with systemctl status php8.3-fpm that the reload hook actually fired. Only once both checks pass cleanly does the configuration belong in production.
# Check syntax and triggers without changing anything
$ logrotate -d /etc/logrotate.d/myapp
reading config file /etc/logrotate.d/myapp
Allocating hash table for state file, size 15360 B
Handling 1 logs
rotating pattern: /var/www/myapp/var/log/app.log
100M bytes in size OR forced from command line (14 rotations)
empty log files are not rotated, old logs are removed
considering log /var/www/myapp/var/log/app.log
log needs rotating
rotating log /var/www/myapp/var/log/app.log, log->rotateCount is 14
dateext suffix '-20260712'
glob pattern '-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
renaming /var/www/myapp/var/log/app.log to /var/www/myapp/var/log/app.log-20260712
running postrotate script
compressing log with: /bin/gzip
# Force an actual rotation (test in staging first)
$ logrotate -f /etc/logrotate.d/myapp
$ ls -la /var/www/myapp/var/log/
8. Automation, the status file, and deployment
On modern distributions logrotate no longer necessarily runs via /etc/cron.daily/, but through the logrotate.timer timer unit, which you can inspect with systemctl list-timers | grep logrotate. The status file /var/lib/logrotate/status records the timestamp of the last rotation for each log path, so a log does not get rotated twice in one day if logrotate happens to run more than once for some reason. A common mistake during manual testing: deleting this status file forces logrotate to rotate all configured logs at once on the next run, which creates unnecessary load in production.
For server fleets running several identically structured applications, it pays off to manage the logrotate configuration as part of the deployment process rather than maintaining it manually on every server. A configuration management tool such as Ansible can generate the file from a template and immediately validate it afterward with logrotate -d, before marking it as valid. That keeps rotation consistent across every environment, and changes to the configuration can be versioned and tracked like any other code change.
# Ansible task: deploy the logrotate configuration and validate it right away
- name: Deploy logrotate configuration for myapp
ansible.builtin.template:
src: templates/logrotate-myapp.j2
dest: /etc/logrotate.d/myapp
owner: root
group: root
mode: "0644"
notify: validate logrotate config
- name: Handler to validate the configuration
ansible.builtin.command:
cmd: logrotate -d /etc/logrotate.d/myapp
listen: validate logrotate config
changed_when: false
9. logrotate compared: common mistakes vs. correct configuration
Most logrotate problems are not caused by missing functionality, but by incomplete configurations that go unnoticed during testing and only surface weeks later in production. The following overview compares the most common failure points against the recommended solution.
| Task | Common mistake | Correct configuration | Benefit |
|---|---|---|---|
| Rotation trigger | only monthly, no size limit |
daily + maxsize 100M |
prevents log surges between cycles |
| File handle after rotation | no postrotate hook |
reload/signal inside postrotate |
disk space is actually freed |
| Permissions of new file | default create without owner |
create 0640 www-data www-data |
application can keep writing right away |
| Testing the configuration | deploy directly, wait for cron | logrotate -d before rollout |
errors are visible immediately, not in production |
| Retention | no rotate N set |
rotate 14 + compress |
disk usage stays bounded |
Notably, almost every one of these mistakes only becomes visible with a delay: a missing postrotate hook shows up when df -h reports no freed space despite a completed rotation, because the old process keeps writing into the deleted but still open file. Whoever consistently combines the right trigger, hook, and testing phase avoids exactly this class of hard to reproduce production problems.
Mironsoft
Server administration, log management, and deployment automation
Log files that will not become a threat to your server?
We review existing log rotation, set up robust logrotate configurations with size and time triggers plus clean postrotate hooks, and test them in a controlled way before they run in production.
Log audit
Map existing log paths, growth patterns, and risks across all servers
Configuration
Set up matching logrotate rules with compression and reload hooks
Deployment
Roll out log rotation as versioned configuration management
10. Summary
Log rotation with logrotate solves a problem that is routinely underestimated in production environments: without a limit, log files grow until a partition fills up and services such as MySQL or PHP-FPM can no longer write. Combining a time based and a size based trigger, for example daily together with maxsize 100M, covers both predictable and sudden log growth. Compression with compress and delaycompress drastically reduces storage needs without endangering writes during the rotation window.
The decisive piece for a working configuration is the postrotate hook, which prompts the application to reopen its file handle after rotation, because otherwise no disk space is actually freed even though the rotation formally succeeded. Every rollout should be preceded by a test with logrotate -d to catch syntax errors and wrong triggers immediately, instead of discovering them days later in production.
Log rotation with logrotate, the essentials at a glance
Combine triggers
daily + maxsize 100M covers predictable and sudden log growth at the same time.
Hooks for log handles
postrotate with reload or signal, otherwise no disk space is actually freed.
Safe permissions
create 0640 www-data www-data prevents write errors right after rotation.
Test before rollout
logrotate -d checks syntax and triggers without touching files or running hooks.