Logrotate is the standard log rotation utility on RHEL 8, responsible for automatically compressing, archiving, and pruning log files before they consume excessive disk space. Without it, long-running applications like NGINX, custom services, and Java applications can silently fill a filesystem partition and cause outages. Logrotate ships with RHEL 8 and is already configured for most system logs under /etc/logrotate.d/; the skill is in writing correct configuration for your own application logs. This tutorial covers writing a complete logrotate configuration for an application, testing it safely, forcing a rotation, and comparing the systemd timer versus the traditional cron approach.
Prerequisites
- A RHEL 8 server with
logrotateinstalled (dnf install -y logrotate— it is present by default) - An application writing logs to a known directory (examples use
/var/log/myapp/) - NGINX installed if you want to test the
postrotatesignal example verbatim - Basic familiarity with systemd services and unit files
Step 1 — Write the Logrotate Configuration File
Drop a dedicated configuration file into /etc/logrotate.d/ for each application. Logrotate reads all files in that directory during each run. The configuration below demonstrates the most important directives for a production application log.
sudo tee /etc/logrotate.d/myapp > /dev/null <&1 | head -30
Step 2 — Understand the Key Directives
Each directive in the configuration above has a specific purpose. Understanding them prevents common mistakes such as silently dropping logs or accumulating unlimited rotated archives.
# Quick reference — run to confirm logrotate version and compiled-in paths
logrotate --version
logrotate --help
# View the current logrotate state file (tracks last rotation dates)
sudo cat /var/lib/logrotate/logrotate.status | head -20
# The 'create' directive sets permissions on the NEW empty log file created
# after rotation. Format: create
# Example: create 0640 nginx nginx
# 'delaycompress' skips compression on the most recently rotated file,
# keeping it uncompressed for one extra rotation cycle. This prevents losing
# log lines that a process may still be writing to the previous log file
# after the rotation event has already occurred.
Step 3 — Test the Configuration with Debug Mode
The --debug flag (or the older -d) performs a dry run: logrotate reads the configuration, evaluates whether rotation would occur, and prints every decision it would make — without touching any files. This is the safe way to validate a new configuration before letting it run automatically.
# Dry-run all configurations — nothing is written to disk
sudo logrotate --debug /etc/logrotate.conf 2>&1 | grep -E "considering|rotating|not rotating"
# Dry-run only the myapp configuration
sudo logrotate --debug /etc/logrotate.d/myapp
# Common output to look for:
# "considering log /var/log/myapp/access.log"
# "rotating log ... forced from command line" ← shows what would happen
# "not rotating, log is empty" ← notifempty is working
Step 4 — Force an Immediate Rotation
Use --force (or -f) to trigger rotation unconditionally regardless of the age or size of the log file. This is useful when deploying a new configuration and wanting to confirm the postrotate script executes correctly and the application continues writing to the new log file without errors.
# Ensure the log directory and a test log file exist
sudo mkdir -p /var/log/myapp
sudo touch /var/log/myapp/access.log
sudo chown -R nginx:nginx /var/log/myapp
# Force rotation NOW — this WILL rename/compress live files
sudo logrotate --force /etc/logrotate.d/myapp
# Confirm the rotated archive was created
ls -lh /var/log/myapp/
# Confirm the application log was recreated with the correct permissions
stat /var/log/myapp/access.log
Step 5 — systemd Timer vs Cron: How Logrotate is Scheduled
On RHEL 8, logrotate is triggered by a systemd timer unit — logrotate.timer — rather than a cron job. The timer fires once per day. You can inspect it, override its schedule, or fall back to a cron entry if your workflow requires more precise timing such as hourly rotation for high-volume logs.
# Inspect the built-in systemd timer
sudo systemctl cat logrotate.timer
sudo systemctl list-timers logrotate.timer
# See when logrotate last ran and what it did
sudo journalctl -u logrotate.service --no-pager | tail -20
# Override the timer schedule to run hourly (creates a drop-in override)
sudo systemctl edit logrotate.timer
# In the editor, add:
# [Timer]
# OnCalendar=hourly
# Alternative: traditional cron entry (runs at 02:30 daily)
echo "30 2 * * * root /usr/sbin/logrotate /etc/logrotate.conf"
| sudo tee /etc/cron.d/logrotate-custom
Step 6 — Handle Applications That Cannot Reopen Log Files (copytruncate)
Some applications — typically older daemons or those without a signal handler — hold their log file descriptor open indefinitely and never switch to a new file after rotation. The copytruncate directive handles this: logrotate copies the current log to the rotated filename, then truncates the original file to zero bytes in place. The application never notices. Use this only when sending a reload signal is not possible, because there is a small window during which log entries may be missed.
sudo tee /etc/logrotate.d/myapp-norehup > /dev/null <<'EOF'
/var/log/myapp/app.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
# NOTE: do NOT use 'create' with 'copytruncate' — the original file
# is kept in place so a new one is never created
}
EOF
sudo logrotate --debug /etc/logrotate.d/myapp-norehup
Conclusion
You now have a solid, production-ready logrotate configuration for application logs on RHEL 8. The daily / rotate 14 combination retains two weeks of compressed history, delaycompress avoids race conditions with applications still writing to the previous log, and the postrotate block ensures NGINX reopens its file descriptors immediately after each rotation. The debug mode and forced-rotation workflow give you a safe way to validate and test any future configuration changes before they run automatically through the systemd timer. For the minority of applications that cannot handle SIGHUP, copytruncate provides a compatible fallback at the cost of a very small potential log gap.
Next steps: Centralizing RHEL 8 Logs with Rsyslog and Remote Syslog Servers, Setting Up Auditd for Security Audit Logging on RHEL 8, and Monitoring Log File Growth with Prometheus and Alertmanager.