Log files are the primary diagnostic tool for server administrators — but without rotation they become a problem in their own right. An unrotated /var/log/nginx/access.log on a busy server can grow to tens of gigabytes within weeks, filling the filesystem, crashing the logging application, and making the log itself unusable because no tool can efficiently process a 50 GB text file. logrotate solves this by periodically archiving log files: renaming the current file, optionally compressing the archive, creating a fresh empty log file, and deleting archives beyond a configured retention period. On RHEL 9, logrotate is installed by default and runs daily via a systemd timer (or cron job), managing logs for dozens of system services out of the box. This guide covers understanding the existing configuration, writing custom rules for application logs, testing without rotating, debugging common problems, and configuring advanced options like per-size rotation and pre/post-rotate scripts.

Prerequisites

  • RHEL 9 server with root or sudo access
  • logrotate installed (included by default)

Step 1 — Verify logrotate is Installed and Running

# Confirm installation
rpm -q logrotate

# Check the systemd timer
systemctl list-timers logrotate*

# Or check the legacy cron job
cat /etc/cron.daily/logrotate

On RHEL 9, logrotate runs as a systemd timer named logrotate.timer that triggers daily.

Step 2 — Understand the Configuration File Hierarchy

  • /etc/logrotate.conf — main configuration file with global defaults
  • /etc/logrotate.d/ — drop-in directory; one file per application. All files in this directory are included by logrotate automatically.

View the global defaults:

cat /etc/logrotate.conf

Common global settings:

# Rotate weekly
weekly

# Keep 4 rotated files
rotate 4

# Create new empty log files after rotation
create

# Use date as suffix instead of sequential numbers (e.g. access.log-20250519)
dateext

# Compress rotated files with gzip
compress

# Include drop-in files
include /etc/logrotate.d

Step 3 — View Existing Application Configurations

ls /etc/logrotate.d/
cat /etc/logrotate.d/nginx

A typical Nginx logrotate configuration:

/var/log/nginx/*.log {
    daily
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    create 0640 nginx adm
    sharedscripts
    postrotate
        if [ -f /run/nginx.pid ]; then
            kill -USR1 $(cat /run/nginx.pid)
        fi
    endscript
}

Step 4 — Write a Custom logrotate Configuration

Create a configuration for a custom application that writes to /var/log/myapp/:

vi /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    # Rotate daily
    daily

    # Keep 30 days of archives
    rotate 30

    # Compress rotated files
    compress

    # Delay compression by one rotation cycle (so the current log is not compressed)
    # This allows the application to finish writing before compressing
    delaycompress

    # Do not error if log file is missing
    missingok

    # Do not rotate if the file is empty
    notifempty

    # Create a new log file after rotation with specified permissions
    create 0640 appuser appgroup

    # Use date suffix (e.g., myapp.log-20250519)
    dateext
    dateformat -%Y%m%d

    # Run pre/postrotate scripts once even if multiple log files match
    sharedscripts

    # Commands to run before rotation begins
    prerotate
        # Custom pre-rotation action (optional)
        echo "Starting log rotation for myapp" >> /var/log/myapp/rotate.log
    endscript

    # Commands to run after rotation completes
    postrotate
        # Signal the application to reopen its log files
        # For systemd-managed services:
        systemctl kill --kill-who=main --signal=HUP myapp 2>/dev/null || true
        # Or reload via PID file:
        # kill -HUP $(cat /run/myapp.pid) 2>/dev/null || true
    endscript
}

Step 5 — Rotate by File Size Instead of Time

For high-volume logs that can grow very large within a single day, use size-based rotation:

/var/log/myapp/access.log {
    # Rotate when file reaches 100 MB
    size 100M

    # Keep 10 rotated files
    rotate 10

    compress
    delaycompress
    missingok
    notifempty
    create 0640 appuser appgroup

    postrotate
        systemctl kill --kill-who=main --signal=HUP myapp 2>/dev/null || true
    endscript
}

Note: size-based rotation only triggers when logrotate runs. If you need more frequent checks, run logrotate via a custom systemd timer (see Step 8).

Step 6 — Configure Retention by Days Instead of Count

The rotate N directive keeps N rotated files. With dateext and daily rotation, that equals N days. For explicit time-based deletion:

/var/log/myapp/*.log {
    daily
    rotate 90          # Keep 90 daily rotations = 90 days retention
    compress
    dateext
    missingok
    notifempty
    create 0640 appuser appgroup
}

Step 7 — Test Without Actually Rotating

Always test a new logrotate configuration before relying on it:

# Dry run — show what would happen without actually rotating
logrotate -d /etc/logrotate.d/myapp

# Force rotation now (even if rotation is not due) — useful for testing
logrotate -f /etc/logrotate.d/myapp

# Verbose output to see what logrotate is doing
logrotate -v /etc/logrotate.conf

Step 8 — Run logrotate More Frequently with a Custom Timer

The default daily timer is sufficient for most logs. To run hourly for high-volume logs:

mkdir -p /etc/systemd/system/logrotate.timer.d
vi /etc/systemd/system/logrotate.timer.d/hourly.conf
[Timer]
OnCalendar=
OnCalendar=hourly
systemctl daemon-reload
systemctl restart logrotate.timer

Step 9 — View logrotate Status and History

logrotate maintains a state file at /var/lib/logrotate/logrotate.status:

# View the last rotation date for each log file
cat /var/lib/logrotate/logrotate.status

# View recent logrotate journal entries
journalctl -u logrotate --since today

Common directives reference

  • daily / weekly / monthly — rotation frequency
  • size SIZE — rotate when file exceeds SIZE (e.g., 100M, 1G)
  • rotate N — keep N rotated archives
  • compress — compress archived files with gzip
  • delaycompress — compress the previous rotation, not the current one
  • missingok — no error if log file does not exist
  • notifempty — skip rotation if file is empty
  • create MODE USER GROUP — create new empty log file after rotation
  • dateext — use date in the rotated filename
  • sharedscripts — run pre/postrotate once for all matching files
  • copytruncate — copy then truncate instead of rename; use for apps that cannot reopen log files

Troubleshooting

  • Log file not rotating despite size exceeding threshold — logrotate runs only when triggered by its timer. If using size-based rotation, run logrotate -f /etc/logrotate.d/myapp manually or increase the timer frequency.
  • Application writing to the old renamed log file after rotation — the app has the file handle open. Use postrotate to send HUP to the process (to reopen log files), or use copytruncate instead of rename-based rotation.
  • Permission denied creating new log file — the create directive requires logrotate to run as a user that can create files in the log directory. Ensure the directory is writable by root or adjust permissions.

Conclusion

Your RHEL 9 server now has comprehensive log rotation configured: system logs are managed by the default logrotate rules, your custom application logs rotate daily with 30-day retention and proper compression, and the application is signalled to reopen its log file handles after rotation. The logrotate -d dry-run command lets you test any changes safely before they take effect.

Next steps: How to Use journalctl for Systemd Log Analysis on RHEL 9, How to Perform a System Security Audit with auditd on RHEL 9, and How to Monitor System Resources with htop and vmstat on RHEL 9.