Optimize Disk Space: Implement Hourly Rotation with Logrotate on Linux

If you are used to managing an infrastructure with many Linux-based OS servers, managing the rotation of log files is crucial because it prevents any application from producing log files that just sit there and accumulate over time.

The log files then become increasingly larger and gradually occupy a significant amount of disk space. Especially, in the cloud environment, when cost is a matter, for example, you have many servers on AWS, and they are attached to their own EBS, and the disk space of those EBSs is taken largely due to those log files. This can translate to storage costs, which are wasteful if not managed properly. Or in worst cases, it causes damage to the server/application because the disk space is full if we wouldn’t notice in time.

NOTED: if you want to know more about other strategies in optimising AWS cost, particularly in the EBS aspect, I’ve written AWS EBS Cost Optimization: 10 Tips to Reduce Your AWS Bill, hope it help you in some cases.

We might have known that Linux has a powerful tool called logrotate, which we can use to manage logs. In this blog, I won’t deep dive into how to create logrotate, because you could easily find it on the internet. However, I want to share a use case where we could utilize logrotate to manage large-sized log files hourly, which you might find hard to look for implementation.

Use Case

Imagine that you have an application (PHP, Python, Java, etc.) or any software running on a Linux OS server (even a Docker container). My case is Ubuntu. Normally, the app produces a log file of a normal size, for example, 10MB every day. It seems normal, but when we release a new application’s feature that accidentally floods the log with warning messages caused by a bug, the log file increases dramatically fast, for example, 1GB every hour. If we keep the default rotation of the log, which runs every day, we would have a 24GB log file size by the time the system rotates the log file. This is just an example; it could be massively larger in real cases depending on how the app produces logs, and in worst cases, this would lead to disruption of the application.

If we know that the log is not too important to keep for a long period, or we have a centralized log system, where the application’s log is sent immediately once it’s produced, then the current log file on the server becomes redundant. We can consider applying logrotate hourly in this case so that we always keep the log file as small as we can. That could help avoid the full disk space issue somewhat.

Configuring logrotate hourly

In the logrotate hourly man page, it seems to be vaguely to me of how to implement this initially. According to the man page:

Normally, logrotate is run as a daily cron job. It will not modify a log more than once in one day unless the criterion for that log is based on the log’s size and logrotate is being run more than once each day, or unless the -f or –force option is used.

On Ubuntu or Debian, logrotate‘s rotation is controlled by systemd (we have two files logrotate.service and logrotate.timer) and it is scheduled to run at 00:00:00 daily.

binh@web-01:~$ sudo systemctl list-timers 
NEXT LEFT LAST PASSED UNIT ACTIVATES
Fri 2025-09-12 14:58:58 NZST 2min 53s Sun 2025-08-24 08:00:25 NZST - motd-news.timer motd-news.service
...
Sat 2025-09-13 00:00:00 NZST 9h Fri 2025-09-12 14:19:47 NZST 36min ago logrotate.timer logrotate.service
....

So, to configure hourly rotation with logrotate, we basically need to do two things:

  • Explicitly enable hourly option either in the logrotate configuration part for each log file (recommended) or in the main /etc/logrotate.conf file (applying globally – not ideal)
  • Create a separate systemd service and systemd timer dedicated to the logrotate’s hourly config only. An alternative is to use crontab, but I wouldn’t recommend this.

Create a demo app to produce log file

To demonstrate this example, I create a small python script to produce log to /var/log/app-demo.log.

Create a directory to store the script:

sudo mkdir -p /data/app/

On ubuntu server, run sudo nano /data/app/app-demo.py and paste the following content:

import logging
import time
import random
import os

def setup_logger(log_file="demo.log"):

    # Create a log file if not exist
    if not os.path.isfile(log_file):
        os.system(f"touch {log_file}")

    # Configure logging
    logging.basicConfig(
        level=logging.DEBUG,  # Capture DEBUG and above
        format="%(asctime)s [%(levelname)s] %(message)s",
        handlers=[
            logging.FileHandler(log_file),   # Write to file
            logging.StreamHandler()          # Also print to console
        ]
    )

def demo_logging():
    count = 0
    while True:
        logging.debug(f"Processing iteration {count}...")
        
        if count % 5 == 0:
            logging.info("Heartbeat: system is running smoothly.")

        if random.random() < 0.2:
            logging.warning("Potential issue detected (simulated).")

        if random.random() < 0.05:
            try:
                raise ValueError("Simulated random error!")
            except Exception as e:
                logging.error("Error occurred", exc_info=True)

        time.sleep(1)  # slow down logs so file doesn’t grow too fast
        count += 1

if __name__ == "__main__":
    setup_logger("/var/log/app-demo.log")
    demo_logging()

Press Ctrl + O and press Enter to save the content. Then press Ctrl + X to exit.

Start the app in the background:

nohup sudo python3 /data/app/app-demo.py

We can test the log’s output by running:

sudo tail -100f /data/app/app-demo.py

Output will look like:

2025-09-12 14:25:53,340 [DEBUG] Processing iteration 0…
2025-09-12 14:25:53,341 [INFO] Heartbeat: system is running smoothly.
2025-09-12 14:25:54,347 [DEBUG] Processing iteration 1…
2025-09-12 14:25:54,348 [WARNING] Potential issue detected (simulated).
2025-09-12 14:25:55,351 [DEBUG] Processing iteration 2…
2025-09-12 14:25:56,354 [DEBUG] Processing iteration 3…
2025-09-12 14:26:02,367 [ERROR] Error occurred
Traceback (most recent call last):
File "/data/app/log-demo.py", line 32, in demo_logging
raise ValueError("Simulated random error!")
ValueError: Simulated random error!
2025-09-12 14:26:03,378 [DEBUG] Processing iteration 10…
......

Enable hourly option

Create a /etc/logrotate.d/hourly directory to store any configuration file of services that we want to rotate hourly.

sudo mkdir /etc/logrotate.d/hourly

And run sudo nano /etc/logrotate.d/hourly/app-demo-log and add the content like:

# use the adm group by default, since this is the owning group
# of /var/log/. Similar to /etc/logrotate.conf
su root adm

/var/log/app-demo.log {
    rotate 24
    hourly
    create 0664 root root
    missingok
    compress
    delaycompress
    dateext
    dateformat -%Y%m%d-%H
}

Press Ctrl + O and press Enter to save the content. Then press Ctrl + X to exit.

Options’ explanation:

  • rotate: number of the rotated log files that should be kept before the old files are removed
  • hourly: explicitly enable hourly rotation – we should combine with dateformat -%Y%m%d-%H
  • create: immediately creates a new log file app-demo.log with the specified mode/owner/group permissions after the old file is rotated. If you exclude, it will be “root adm” as su command.
  • missingok: continues rotating to the next log file (if available, e.g. second-app-demo.log) if the current log file (app-demo.log in this case) does not exist without issuing any error.
  • compress: compresses the old log files with /bin/gzip
  • delaycompress: postpone the compression of the previous log file to next rotation. For example, the 1st rotation, app-demo.log turns into app-demo.log-%Y%m%d-%H, logrotate won’t compress the old log file yet. At the 2nd rotation run, app-demo.log-%Y%m%d-%H turns into app-demo.log-%Y%m%d-%H.gz
  • dateext: Archive old versions of log files by adding dateformat extention instead of just a number. E.g., app-demo.log turns into app-demo.log-%Y%m%d-%H instead of app-demo.log.1
  • dateformat: Specify the extension for dateext above.

For other services, we can create similar files with other options if need. Please see man page to know more about options.

Setting up a separate systemd service and timer for logrotate-hourly

1. Create a /etc/systemd/system/logrotate-hourly.service, we run:

sudo nano /etc/systemd/system/logrotate-hourly.service

And add the following content:

[Unit]
Description=Service to run hourly logrotates only
Documentation=man:logrotate(8) man:logrotate.conf(5)
RequiresMountsFor=/var/log
ConditionACPower=true

[Service]
Type=oneshot
ExecStart=/usr/bin/flock --wait 21600 /run/lock/logrotate.service /usr/sbin/logrotate /etc/logrotate.d/hourly

# performance options
Nice=19
IOSchedulingClass=best-effort
IOSchedulingPriority=7

# hardening options
#  details: https://www.freedesktop.org/software/systemd/man/systemd.exec.html
#  no ProtectHome for userdir logs
#  no PrivateNetwork for mail deliviery
#  no NoNewPrivileges for third party rotate scripts
#  no RestrictSUIDSGID for creating setgid directories
LockPersonality=true
MemoryDenyWriteExecute=true
PrivateDevices=true
PrivateTmp=true
ProtectClock=true
ProtectControlGroups=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectSystem=full
RestrictNamespaces=true
RestrictRealtime=true

Press Ctrl + O and press Enter to save the content. Then press Ctrl + X to exit.

The config above is a copied version of /lib/systemd/system/logrotate.service with its own ExecStart and Description. I’m using Ubuntu 24.04, in case the above config wouldn’t work with previous OS versions. We can also run:

sudo cp /lib/systemd/system/logrotate.service /etc/systemd/system/logrotate-hourly.service

And modify ExecStart and Description in the /etc/systemd/system/logrotate-hourly.service according to what I set above. That is to ensure other settings will be working at your OS version.

2. Create a /etc/systemd/system/logrotate-hourly.timer file, we run:

sudo nano /etc/systemd/system/logrotate-hourly.timer

And add this content

[Unit]
Description=Timer to run hourly logrotates only
Documentation=man:logrotate(8) man:logrotate.conf(5)

[Timer]
OnCalendar=hourly
AccuracySec=12h
Persistent=true

[Install]
WantedBy=timers.target

Press Ctrl + O and press Enter to save the content. Then press Ctrl + X to exit.

3. Enable flock command for logrotate.service to prevent logrotate from running simultaneously with logrotate-hourly.service

# Create a drop-in directory
sudo mkdir /etc/systemd/system/logrotate.service.d

# Append a drop-in file to mutate the ExecStart option in the logrotate.service
sudo nano /etc/systemd/system/logrotate.service.d/logrotate-flock.conf

Add the following content to /etc/systemd/system/logrotate.service.d/logrotate-flock.conf:

[Service]
ExecStart=
ExecStart=/usr/bin/flock --wait 21600 /run/lock/logrotate.service /usr/sbin/logrotate /etc/logrotate.conf

Press Ctrl + O and press Enter to save the content. Then press Ctrl + X to exit.

Reload systemd daemon, and start/enable logrotate-hourly serivce and timer, run:

sudo systemctl daemon-reload
sudo systemctl start logrotate-hourly.service
sudo systemctl start logrotate-hourly.timer
sudo systemctl enable logrotate-hourly.service
sudo systemctl enable logrotate-hourly.timer

ExecStart‘s explanation:

When the logrotate process runs, flock command creates a /run/lock/logrotate.service file temporarily then. That is to ensure that the logrotate process, started by either logrotate-hourly.service or logrotate.service, waits for the first process to finish before the next one can continue. If the next one waits for 21600 seconds and hasn’t been able to acquire the /run/lock/logrotate.service file, it stopped then.

Checking whether systemd timer shows a schedule for logrotate-hourly.service or not, run:

sudo systemctl list-timers | grep logrotate

Output:

Fri 2025-09-12 17:00:00 NZST      6min -                                       - logrotate-hourly.timer         logrotate-hourly.service
Sat 2025-09-13 00:00:00 NZST 7h Fri 2025-09-12 14:19:47 NZST 2h 34min ago logrotate.timer logrotate.service

To test /etc/logrotate.d/hourly/app-demo-log file to make sure the configuration is correct, we can force to run logrotate:

sudo logrotate -f -v /etc/logrotate.d/hourly/

Output:

-rw-rw-r-- 1 root root       0 Sep 12 17:31 /var/log/app-demo.log
-rw-r--r-- 1 root root 1082505 Sep 12 17:32 /var/log/app-demo.log-20250912-17

If we wait for a few hours to see whether logrotate-hourly.timer triggers rotation, we will have result looks like:

-rw-rw-r-- 1 root root      0 Sep 12 20:00 /var/log/app-demo.log
-rw-r--r-- 1 root root 111812 Sep 12 17:34 /var/log/app-demo.log-20250912-17.gz
-rw-rw-r-- 1 root root  50425 Sep 12 19:00 /var/log/app-demo.log-20250912-18.gz
-rw-rw-r-- 1 root root     20 Sep 12 18:00 /var/log/app-demo.log-20250912-19.gz
-rw-rw-r-- 1 root root      0 Sep 12 19:00 /var/log/app-demo.log-20250912-20

Ok. That’s all we need for this configuration!

Noted: for configuring logrotate with a cron job, if you copy content from /etc/cron.daily/logrotate to /etc/cron.hourly/logrotate, and modify to run logrotate hourly, e.g. something similar to:

#!/bin/sh

# skip in favour of systemd timer
if [ -d /run/systemd/system ]; then
exit 0
fi

# this cronjob persists removals (but not purges)
if [ ! -x /usr/sbin/logrotate ]; then
exit 0
fi

/usr/sbin/logrotate /etc/logrotate.d/hourly
EXITVALUE=$?
if [ $EXITVALUE != 0 ]; then
/usr/bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]"
fi
exit $EXITVALUE

This won’t work because the /run/systemd/system directory already exists, and your script will exit at the first check then. Or if it detects a logrotate process is running, the script also exits without continuing further (at the 2nd check). As I mentioned at the beginning, logrotate is managed and run by systemd. Therefore, if we want to use this cron job, we have to disable the systemd configuration for logrotate then.


Discover more from Turn DevOps Easier

Subscribe to get the latest posts sent to your email.

By Binh

Leave a Reply

Your email address will not be published. Required fields are marked *

Content on this page