Introduction
logrotate prevents log files from consuming all disk space by rotating, compressing, and deleting old logs on a schedule. While every Linux system ships with logrotate, managing configurations across dozens of servers with different applications requires automation. Ansible templates logrotate configs consistently — ensuring every server has the right rotation policy for each application.
How logrotate Works
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ /etc/logrotate │────►│ logrotate │────►│ Rotated logs │
│ .conf + .d/* │ │ (daily cron) │ │ .1, .2.gz ... │
└─────────────────┘ └──────────────────┘ └──────────────────┘
- Global config:
/etc/logrotate.conf - App configs:
/etc/logrotate.d/*.conf - Runs via: daily cron or systemd timer
Basic logrotate with Ansible
Deploy a logrotate Config
---
- name: Configure logrotate
hosts: servers
become: true
tasks:
- name: Install logrotate
ansible.builtin.package:
name: logrotate
state: present
- name: Configure application log rotation
ansible.builtin.copy:
dest: /etc/logrotate.d/myapp
content: |
/var/log/myapp/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 0640 myapp myapp
sharedscripts
postrotate
systemctl reload myapp 2>/dev/null || true
endscript
}
mode: '0644'
Template-Based Configuration
Variables
# group_vars/webservers/logrotate.yml
logrotate_configs:
- name: nginx
paths:
- /var/log/nginx/*.log
frequency: daily
rotate: 14
compress: true
delaycompress: true
create: "0640 www-data adm"
postrotate: "nginx -s reopen"
- name: myapp
paths:
- /var/log/myapp/*.log
- /var/log/myapp/workers/*.log
frequency: daily
rotate: 30
compress: true
maxsize: "100M"
create: "0640 myapp myapp"
postrotate: "systemctl reload myapp"
- name: syslog-extra
paths:
- /var/log/cron.log
- /var/log/auth.log
frequency: weekly
rotate: 52
compress: true
create: "0640 syslog adm"
Template
{# templates/logrotate.conf.j2 #}
{{ item.paths | join('\n') }} {
{{ item.frequency | default('daily') }}
missingok
rotate {{ item.rotate | default(14) }}
{% if item.compress | default(true) %}
compress
{% if item.delaycompress | default(true) %}
delaycompress
{% endif %}
{% endif %}
notifempty
{% if item.maxsize is defined %}
maxsize {{ item.maxsize }}
{% endif %}
{% if item.minsize is defined %}
minsize {{ item.minsize }}
{% endif %}
{% if item.maxage is defined %}
maxage {{ item.maxage }}
{% endif %}
{% if item.create is defined %}
create {{ item.create }}
{% endif %}
{% if item.su is defined %}
su {{ item.su }}
{% endif %}
{% if item.dateext | default(false) %}
dateext
dateformat -%Y%m%d
{% endif %}
{% if item.copytruncate | default(false) %}
copytruncate
{% endif %}
sharedscripts
{% if item.prerotate is defined %}
prerotate
{{ item.prerotate }}
endscript
{% endif %}
{% if item.postrotate is defined %}
postrotate
{{ item.postrotate }} 2>/dev/null || true
endscript
{% endif %}
}
Playbook
---
- name: Deploy logrotate configurations
hosts: all
become: true
tasks:
- name: Install logrotate
ansible.builtin.package:
name: logrotate
state: present
- name: Deploy logrotate configs
ansible.builtin.template:
src: logrotate.conf.j2
dest: "/etc/logrotate.d/{{ item.name }}"
mode: '0644'
validate: "logrotate -d %s"
loop: "{{ logrotate_configs | default([]) }}"
loop_control:
label: "{{ item.name }}"
- name: Remove unmanaged logrotate configs
ansible.builtin.find:
paths: /etc/logrotate.d
patterns: "*.conf"
excludes: "{{ logrotate_configs | map(attribute='name') | list }}"
register: unmanaged
when: logrotate_cleanup | default(false)
Common Application Configs
Nginx
- name: nginx
paths: ["/var/log/nginx/*.log"]
frequency: daily
rotate: 14
compress: true
delaycompress: true
create: "0640 www-data adm"
postrotate: "invoke-rc.d nginx rotate"
Docker Containers
- name: docker-containers
paths: ["/var/lib/docker/containers/*/*.log"]
frequency: daily
rotate: 7
compress: true
copytruncate: true
maxsize: "100M"
PostgreSQL
- name: postgresql
paths: ["/var/log/postgresql/*.log"]
frequency: weekly
rotate: 12
compress: true
create: "0640 postgres adm"
postrotate: "pg_ctl reload -D /var/lib/postgresql/16/main"
systemd Journal (Size Limit)
- name: Configure journald max size
ansible.builtin.lineinfile:
path: /etc/systemd/journald.conf
regexp: '^#?SystemMaxUse='
line: "SystemMaxUse=500M"
notify: restart journald
handlers:
- name: restart journald
ansible.builtin.service:
name: systemd-journald
state: restarted
Global logrotate.conf
- name: Configure global logrotate settings
ansible.builtin.template:
src: logrotate-global.conf.j2
dest: /etc/logrotate.conf
mode: '0644'
{# templates/logrotate-global.conf.j2 #}
# Global logrotate configuration - managed by Ansible
weekly
rotate 4
create
dateext
compress
# Include app-specific configs
include /etc/logrotate.d
Validate and Test
- name: Validate all logrotate configs
ansible.builtin.command: logrotate -d /etc/logrotate.conf
register: validation
changed_when: false
failed_when: "'error' in validation.stderr"
- name: Force rotation (manual trigger)
ansible.builtin.command: logrotate -f /etc/logrotate.conf
when: force_rotate | default(false)
Monitor Disk Usage
- name: Check log directory sizes
ansible.builtin.command: "du -sh {{ item }}"
register: log_sizes
changed_when: false
loop:
- /var/log
- /var/log/nginx
- /var/log/myapp
- name: Alert if logs exceed threshold
ansible.builtin.assert:
that:
- (item.stdout.split('\t')[0] | human_to_bytes) < 5368709120 # 5GB
fail_msg: "{{ item.item }} is {{ item.stdout.split('\t')[0] }} — exceeds 5GB!"
loop: "{{ log_sizes.results }}"
loop_control:
label: "{{ item.item }}"
logrotate Directives Reference
| Directive | Description |
|---|---|
daily / weekly / monthly | Rotation frequency |
rotate N | Keep N rotated files |
compress | gzip old logs |
delaycompress | Compress on next rotation (not current) |
copytruncate | Copy then truncate (for apps that can't reopen) |
create mode owner group | Create new file with permissions |
missingok | Don't error if log is missing |
notifempty | Don't rotate empty files |
maxsize SIZE | Rotate when file exceeds size |
minsize SIZE | Don't rotate if smaller than size |
maxage N | Delete rotated logs older than N days |
dateext | Use date in rotated filename |
sharedscripts | Run post/pre scripts once for all files |
postrotate | Script to run after rotation |
prerotate | Script to run before rotation |
su user group | Run as specific user (for restricted dirs) |
Troubleshooting
Logs Not Rotating
# Check logrotate status
cat /var/lib/logrotate/status
# Test config (dry run)
logrotate -d /etc/logrotate.d/myapp
# Force rotation
logrotate -f /etc/logrotate.d/myapp
Permission Denied
# Use su directive for restricted directories
- name: app-logs
paths: ["/opt/myapp/logs/*.log"]
su: "myapp myapp"
rotate: 14
compress: true
copytruncate: true
Related Articles
Conclusion
logrotate configuration is a perfect fit for Ansible templates — define rotation policies as variables per host group, deploy them consistently across your fleet, and validate with logrotate -d. Use copytruncate for applications that can't reopen files, postrotate scripts to signal services, and maxsize for high-volume logs that can't wait for the daily cron. Every application that writes logs should have a managed logrotate config.