How to Rotate Django and Gunicorn Logs on Linux
Django and Gunicorn log rotation becomes a production issue as soon as your app writes request logs, error logs, or application logs to disk for more than a few days.
Problem statement
Django and Gunicorn log rotation becomes a production issue as soon as your app writes request logs, error logs, or application logs to disk for more than a few days. Gunicorn access logs can grow quickly on busy sites, and Django app logs often include stack traces, warnings, and background task output. If you do not rotate them, /var/log or your app disk can fill up and cause request failures, database issues, or failed deploys.
Misconfigured rotation is also risky. Renaming a log file is not enough if Gunicorn keeps its file handle open and continues writing to the old rotated file. Bad ownership or create settings can leave Gunicorn or Django unable to write new logs after rotation. The safe production goal is to:
- rotate files before they fill disk
- keep a useful retention window
- preserve permissions
- make Gunicorn reopen log files cleanly
- verify the service continues writing after rotation
Quick answer
Use Linux logrotate for file-based Gunicorn and Django logs, with explicit ownership, compression, retention, and a postrotate action that tells Gunicorn to reopen its log files.
Before trusting the setup in production:
- confirm which logs are actually file-based
- configure a
logrotaterule in/etc/logrotate.d/ - use
createso new files have the right permissions - signal Gunicorn after rotation
- test with
logrotate -dand then force a rotation in a safe window
If Gunicorn logs only to journald or stdout/stderr, file rotation may not be needed for Gunicorn itself.
Step-by-step solution
1. Identify which logs need rotation
Start by listing the log files your deployment actually writes to disk.
Common candidates:
- Gunicorn access log
- Gunicorn error log
- Django application log written via Python logging
Inspect your log directory:
ls -lah /var/log/your-app/
Typical layout:
/var/log/your-app/gunicorn-access.log
/var/log/your-app/gunicorn-error.log
/var/log/your-app/django.log
Do not automatically combine unrelated services into one rule unless ownership and paths are consistent. Keep these separate if needed:
- Nginx logs
- Celery worker logs
- system logs managed by
journald - database logs
Verification checks:
- Confirm every file in your rotation rule is actually written by the same app or service group.
- Confirm you are not trying to rotate
journald-managed logs withlogrotate.
2. Confirm how Gunicorn is writing logs
Check the Gunicorn systemd unit:
systemctl cat gunicorn.service
If your service uses a different unit name, inspect that instead, such as myapp-gunicorn.service or an instance unit like gunicorn@myapp.service.
Also inspect the running process:
ps -ef | grep '[g]unicorn'
Look for flags like:
--access-logfile /var/log/your-app/gunicorn-access.log
--error-logfile /var/log/your-app/gunicorn-error.log
Example systemd snippet:
[Service]
User=appuser
Group=www-data
WorkingDirectory=/srv/your-app/current
ExecStart=/srv/your-app/venv/bin/gunicorn your_project.wsgi:application \
--workers 3 \
--bind 127.0.0.1:8000 \
--access-logfile /var/log/your-app/gunicorn-access.log \
--error-logfile /var/log/your-app/gunicorn-error.log
If Gunicorn logs only to stdout or stderr and systemd captures that into journald, file rotation for Gunicorn may not be needed. In that case, keep this page scoped to your Django file logs only.
Verification checks:
- Confirm the exact access and error log paths.
- If no file paths exist, do not create a file rotation rule for Gunicorn.
3. Choose a safe log directory and permissions model
A common location is:
/var/log/your-app/
Create it if needed:
sudo mkdir -p /var/log/your-app
sudo chown appuser:www-data /var/log/your-app
sudo chmod 0750 /var/log/your-app
Avoid world-readable logs. Production logs may contain:
- session identifiers
- email addresses
- IPs
- stack traces
- tokens or request fragments
Use least privilege. A good default is:
- directory:
0750 - files:
0640
Verification checks:
- The service user can write logs.
- Unrelated local users cannot read log contents.
Rollback note: if changing ownership breaks writes, restore the previous owner and group before reloading or restarting services. On hardened hosts, also verify SELinux or AppArmor policy if permissions look correct but writes still fail.
4. Create a logrotate policy for Django and Gunicorn
Create /etc/logrotate.d/your-app:
/var/log/your-app/gunicorn-access.log
/var/log/your-app/gunicorn-error.log
/var/log/your-app/django.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
dateext
create 0640 appuser www-data
sharedscripts
postrotate
/bin/systemctl kill -s USR1 gunicorn.service >/dev/null 2>&1 || true
endscript
}
Replace gunicorn.service with your actual systemd unit name.
What the main directives do:
daily: rotate once per dayrotate 14: keep 14 archived log setscompress: gzip older logsdelaycompress: leave the most recently rotated file uncompressed until the next cyclemissingok: do not fail if a file is absentnotifempty: skip empty logsdateext: use date-based suffixescreate 0640 appuser www-data: create a new active file with safe permissionssharedscripts: runpostrotateonce for the whole block, not once per file
Some distributions or permission layouts may also require a su directive, but do not assume the app user is always the correct choice for files under /var/log. If you need su, set it deliberately for your host and test it during a forced rotation.
If log growth is bursty, you can prefer size or a combined time-and-size policy if your distro supports it consistently. For many apps, daily is the simpler baseline.
5. Make Gunicorn reopen log files after rotation
Renaming a log file alone is not enough. A running process may keep writing to the old file descriptor even after the file has been moved.
For Gunicorn, a common approach is signaling it with USR1 so it reopens log files:
postrotate
/bin/systemctl kill -s USR1 gunicorn.service >/dev/null 2>&1 || true
endscript
Replace gunicorn.service with your actual unit name.
This is usually safer than a full restart because it avoids unnecessary worker interruption. A restart for log rotation alone is typically excessive and increases deployment risk.
Avoid copytruncate unless you cannot signal the process to reopen logs. On busy services it can lose log lines during rotation.
After rotation, check for deleted-but-still-open files:
sudo lsof | grep gunicorn | grep deleted
Verification checks:
- No Gunicorn process is writing to a deleted rotated file.
- New log entries appear in the new active log file.
If you are not using systemd, signal the Gunicorn master process directly using the method appropriate for your supervisor or process manager.
6. Handle Django application logs correctly
If Django writes to files through Python logging, make sure you do not rotate the same file in two different ways.
A simple Django logging example using WatchedFileHandler on Linux:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
},
},
"handlers": {
"app_file": {
"class": "logging.handlers.WatchedFileHandler",
"filename": "/var/log/your-app/django.log",
"level": "INFO",
"formatter": "standard",
},
},
"root": {
"handlers": ["app_file"],
"level": "INFO",
},
"loggers": {
"django": {
"handlers": ["app_file"],
"level": "INFO",
"propagate": False,
},
"your_app": {
"handlers": ["app_file"],
"level": "INFO",
"propagate": False,
},
},
}
WatchedFileHandler is often a better fit with Linux logrotate than a plain FileHandler, because it notices when the file changes under rotation.
The key rule is to pick one owner for rotation responsibility:
- either Django rotates internally
- or
logrotaterotates the file
Do not use both for the same log file.
Verification checks:
- Your actual app logger writes to the file you plan to rotate.
- Log messages still appear after a forced rotation.
7. Test the log rotation configuration safely
Dry-run first:
sudo logrotate -d /etc/logrotate.d/your-app
Then force a rotation during a low-risk window:
sudo logrotate -f /etc/logrotate.d/your-app
Watch Gunicorn service output and log files:
sudo journalctl -u gunicorn.service -n 50 --no-pager
tail -f /var/log/your-app/gunicorn-error.log
Checks after forcing rotation:
- rotated files exist
- a new active file exists
- ownership and mode are correct
- Gunicorn keeps writing
- no process is writing to a deleted file
Also confirm disk usage:
df -h
du -sh /var/log/your-app/
If the rule fails and logging stops, disable the custom rule and restore the active files to a known-good state:
sudo mv /etc/logrotate.d/your-app /root/your-app.logrotate.disabled
sudo chown appuser:www-data /var/log/your-app/*.log
sudo chmod 0640 /var/log/your-app/*.log
sudo systemctl kill -s USR1 gunicorn.service || sudo systemctl restart gunicorn.service
If your unit name is different, replace gunicorn.service accordingly. If you changed an existing rotation policy, restore the previous file from backup before the next scheduled rotation.
8. Monitor disk usage and retention
Retention depends on debugging needs, compliance, and available disk. A common starting point is 7 to 14 days locally with compression.
Compression reduces space usage, but local rotation is still not centralized logging. If you need long-term search, auditability, or multi-server correlation, ship logs off-server later to a log platform or collector.
Useful periodic checks:
sudo logrotate -d /etc/logrotate.d/your-app
df -h
du -sh /var/log/your-app/
find /var/log/your-app -type f -name '*.gz' | wc -l
9. Notes on automation
Manual logrotate setup works well for one or two servers. It becomes repetitive when you manage multiple Django apps with the same directory layout, service naming pattern, and retention policy.
Good candidates for reusable scripts or templates are:
- log directory creation
- the
/etc/logrotate.d/file - Gunicorn systemd logging arguments
- post-deploy checks like
logrotate -d - ownership and mode validation after rotation
Explanation
This setup works because it covers the full lifecycle of file-based production logs:
logrotatecontrols retention and compressioncreateensures a new writable file exists immediatelyUSR1makes Gunicorn reopen files instead of writing to stale descriptorsWatchedFileHandlerhelps Django detect rotated files cleanly on Linux
Choose alternatives when the architecture is different:
- If you use
journald, rely on journal retention instead of file rotation for Gunicorn. - If you run in containers, host
logrotatemay not apply to container stdout or stderr logs. - If your Django app uses an internal rotating handler, remove that overlap or stop rotating the same file with
logrotate.
Edge cases / notes
- Gunicorn keeps writing to the old rotated file: your
postrotatesignal may be missing, may target the wrong unit, or may not be reaching the Gunicorn master process. - Permission denied after rotation:
createowner or group does not match the process user, the parent directory is too restrictive, or a host security policy such as SELinux or AppArmor is blocking writes. - Multiple services share one log file path: avoid this. Give each service its own file.
- Docker deployments: if Gunicorn logs to stdout or stderr, use the container runtime or orchestrator logging configuration instead of host file
logrotatefor those streams. - Using journald: if Gunicorn is fully journald-based, rotate Django file logs only, or move Django logging there too for consistency.
- Disk already full: do not blindly delete the active open file. First identify what is open, remove or archive old rotated logs carefully, and confirm the process can reopen the active log path.
- Non-systemd deployment: if Gunicorn runs under Supervisor, runit, or another process manager, adapt the reopen signal to that environment instead of copying the
systemctlexample. - Temptation to use
copytruncate: avoid it unless reopen signaling is impossible, because it can drop log lines during busy periods.
Internal links
For background, see Django Deployment Checklist for Production.
Related implementation guides:
For troubleshooting, see Deploy Django ASGI with Uvicorn and Nginx.
FAQ
Should I rotate Django logs with logrotate or Python logging handlers?
Use one rotation mechanism per file. On Linux, logrotate plus Django WatchedFileHandler is a clean production choice for file-based logs.
Do I need to restart Gunicorn after log rotation?
Usually no. A signal such as USR1 is often enough to make Gunicorn reopen log files. Verify this behavior on your deployed version, process manager, and unit setup before relying on it.
What retention period should I use for production logs?
Start with 7 to 14 days on disk, then adjust based on traffic, disk size, debugging needs, and compliance requirements. Compress older files unless you have a strong reason not to.
Is journald better than file-based Gunicorn logging?
It can be simpler on systemd-based hosts because you avoid file rotation for Gunicorn entirely. File logs still make sense when you want explicit app-local files or integration with existing file-based operations.