Operations
#django
#gunicorn
#linux

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:

  1. confirm which logs are actually file-based
  2. configure a logrotate rule in /etc/logrotate.d/
  3. use create so new files have the right permissions
  4. signal Gunicorn after rotation
  5. test with logrotate -d and 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 with logrotate.

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 day
  • rotate 14: keep 14 archived log sets
  • compress: gzip older logs
  • delaycompress: leave the most recently rotated file uncompressed until the next cycle
  • missingok: do not fail if a file is absent
  • notifempty: skip empty logs
  • dateext: use date-based suffixes
  • create 0640 appuser www-data: create a new active file with safe permissions
  • sharedscripts: run postrotate once 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 logrotate rotates 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:

  • logrotate controls retention and compression
  • create ensures a new writable file exists immediately
  • USR1 makes Gunicorn reopen files instead of writing to stale descriptors
  • WatchedFileHandler helps 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 logrotate may 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 postrotate signal may be missing, may target the wrong unit, or may not be reaching the Gunicorn master process.
  • Permission denied after rotation: create owner 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 logrotate for 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 systemctl example.
  • Temptation to use copytruncate: avoid it unless reopen signaling is impossible, because it can drop log lines during busy periods.

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.

2026 ยท django-deployment.com - Django Deployment knowledge base