Deployment
#django
#gunicorn
#systemd

How to Configure systemd for Gunicorn and Django

Running Gunicorn manually for a Django app is fragile. If your SSH session closes, the process can stop.

Problem statement

Running Gunicorn manually for a Django app is fragile. If your SSH session closes, the process can stop. If the server reboots, the app does not come back unless you start it again. You also lose a clean way to restart the service, inspect logs, and control how failures are handled.

A production-safe pattern is to run Gunicorn under systemd. This gives you boot-time startup, supervised restarts, standard service management commands, and centralized logs. This page shows how to configure systemd for Gunicorn and Django on a single Linux server, with a reverse proxy such as Nginx in front.

Quick answer

For a reliable systemd gunicorn django setup:

  1. Run Gunicorn as a dedicated non-root user.
  2. Confirm Gunicorn starts manually from the correct virtualenv.
  3. Create a systemd service unit pointing at the project directory, environment file, and Gunicorn binary.
  4. Bind Gunicorn to a Unix socket or localhost TCP port.
  5. If you use a Unix socket under /run, configure RuntimeDirectory= so it is recreated on boot.
  6. Reload systemd, start the service, and enable it on boot.
  7. Verify with systemctl status and journalctl.
  8. Put Nginx or Caddy in front for proxying, static files, and TLS.

Step-by-step solution

When to use systemd for Gunicorn and Django

systemd is a good fit when your Django app runs directly on a Linux host or VM and you want standard process supervision.

What it solves:

  • starts Gunicorn on boot
  • restarts it after failures
  • provides standard service commands with systemctl
  • stores logs in journald
  • makes deployments repeatable

What it does not replace:

  • reverse proxying
  • TLS termination
  • metrics and alerting
  • backups
  • rollback planning

1. Confirm your paths and runtime user

Use consistent paths before writing the service file. Example layout:

  • app user: django
  • project root: /srv/myproject
  • current release: /srv/myproject/current
  • virtualenv: /srv/myproject/venv
  • socket: /run/gunicorn-myproject/gunicorn.sock

Check ownership:

sudo chown -R django:django /srv/myproject

If Nginx will connect to the Unix socket, a common pattern is to run Gunicorn as user django and group www-data, so the proxy can access the socket.

2. Verify Gunicorn works manually first

Before involving systemd, confirm that Gunicorn can import your Django app from the correct virtualenv.

Check the binary:

/srv/myproject/venv/bin/gunicorn --version

Test a manual start:

sudo mkdir -p /run/gunicorn-myproject
sudo chown django:www-data /run/gunicorn-myproject
cd /srv/myproject/current
/srv/myproject/venv/bin/gunicorn --workers 3 --bind unix:/run/gunicorn-myproject/gunicorn.sock myproject.wsgi:application

If this fails, fix that first. Common failures here are:

  • wrong module path like myproject.wsgi:application
  • missing environment variables
  • missing database settings
  • bad virtualenv path

Stop the manual process after testing.

Verification check: Gunicorn should start without immediate import errors. If it crashes, do not continue to the systemd unit yet.

3. Decide between a Unix socket and a TCP port

For a single host with Nginx on the same machine, prefer a Unix socket:

  • slightly simpler local proxying
  • no exposed listening port
  • common production pattern

Example socket bind:

unix:/run/gunicorn-myproject/gunicorn.sock

Use a TCP port if needed for a different layout, such as localhost proxying or some container edge cases:

127.0.0.1:8000

If you use TCP, bind to localhost only unless you intentionally want external access.

4. Create an environment file

Keep most environment variables out of the service unit. Create a root-owned file:

sudo nano /etc/myproject.env

Example contents:

DJANGO_SETTINGS_MODULE=myproject.settings.production
DJANGO_DEBUG=False
SECRET_KEY=replace-me
DATABASE_URL=replace-me
ALLOWED_HOSTS=example.com,www.example.com
CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.com

Set restrictive permissions:

sudo chown root:root /etc/myproject.env
sudo chmod 600 /etc/myproject.env

Notes:

  • DJANGO_DEBUG=False only works if your project reads that environment variable and maps it to Django’s DEBUG setting.
  • Do not commit real secrets into source control. Use this file only on the server, with real values.

Verification check: confirm the file is not world-readable.

5. Create the Gunicorn systemd service file

Create the service unit:

sudo nano /etc/systemd/system/gunicorn-myproject.service

Use this example django gunicorn systemd service file:

[Unit]
Description=Gunicorn for Django project myproject
After=network.target

[Service]
User=django
Group=www-data
WorkingDirectory=/srv/myproject/current
EnvironmentFile=/etc/myproject.env
RuntimeDirectory=gunicorn-myproject
UMask=0007
ExecStart=/srv/myproject/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/run/gunicorn-myproject/gunicorn.sock \
    myproject.wsgi:application
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Important fields:

  • User: do not run Gunicorn as root
  • Group: set this so Nginx can access the socket if needed
  • WorkingDirectory: should match the Django project location
  • EnvironmentFile: loads settings outside the unit file
  • RuntimeDirectory: creates /run/gunicorn-myproject on boot, which is important because /run is cleared on reboot
  • UMask=0007: helps keep the socket group-accessible while not making it world-accessible
  • ExecStart: full path to Gunicorn in the virtualenv
  • Restart=on-failure: restarts after crashes, not after clean stops

Optional hardening directives, compatibility-dependent:

PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=full

Add these only after testing. If you enable filesystem hardening, also define writable locations explicitly where needed, for example with ReadWritePaths= for media, temp, or release-managed directories.

6. Load, start, and enable the service

Reload systemd after creating the unit:

sudo systemctl daemon-reload

Start the service:

sudo systemctl start gunicorn-myproject

Enable it on boot:

sudo systemctl enable gunicorn-myproject

Check status:

sudo systemctl status gunicorn-myproject

View recent logs:

sudo journalctl -u gunicorn-myproject -n 50 --no-pager

Follow logs live:

sudo journalctl -u gunicorn-myproject -f

Verification checks:

  • systemctl status shows active (running)
  • logs do not show repeated restart loops
  • the socket or localhost port exists as expected

For a Unix socket, verify:

ls -l /run/gunicorn-myproject/gunicorn.sock

7. Connect Nginx to Gunicorn

If you use a Unix socket, make sure the Nginx config points to the same path.

Example snippet:

location / {
    include proxy_params;
    proxy_pass http://unix:/run/gunicorn-myproject/gunicorn.sock;
}

If you use TCP instead, proxy to localhost:

location / {
    include proxy_params;
    proxy_pass http://127.0.0.1:8000;
}

If Nginx returns 502 Bad Gateway, check:

  • Gunicorn is running
  • socket path matches exactly
  • socket permissions allow Nginx access
  • Gunicorn is bound to the expected TCP port or socket
  • the Django app did not crash on startup

Verification check: reload Nginx and request the app through the public web server, not just directly through Gunicorn.

8. Handle deployments safely

A normal release flow should be:

  1. upload or switch code
  2. install dependencies if needed
  3. run migrations
  4. collect static files
  5. restart Gunicorn
  6. verify health
  7. keep the previous release available until checks pass

Restart Gunicorn after a deploy:

sudo systemctl restart gunicorn-myproject

A full restart is usually safer than trying to optimize for partial reload behavior during early deployments.

Example verification after deploy:

sudo systemctl status gunicorn-myproject
sudo journalctl -u gunicorn-myproject -n 50 --no-pager
curl -I http://127.0.0.1/

Rollback plan

If a new release fails:

  1. point your current symlink back to the previous release
  2. restart Gunicorn
  3. verify status and logs
  4. test the site through Nginx

Example if you use release symlinks:

ln -sfn /srv/myproject/releases/previous /srv/myproject/current
sudo systemctl restart gunicorn-myproject

Do not delete the previous release until the new one passes health checks.

If the failed release included backward-incompatible database migrations, switching the current symlink back may not fully restore the app. Treat schema rollback as a separate procedure and test it before relying on it in production.

9. Common systemd and Gunicorn issues

Virtualenv path is wrong

If ExecStart points to a missing Gunicorn binary, the service will fail immediately. Check:

ls -l /srv/myproject/venv/bin/gunicorn

Environment variables are missing

If the app works in a shell but not in systemd, your shell may be loading variables that the service does not. Put required values in EnvironmentFile and restart.

WorkingDirectory or module path is wrong

If myproject.wsgi:application cannot be imported, confirm:

  • WorkingDirectory points at the correct project
  • the Python package name is correct
  • the code exists in the current release path

Socket permission denied

If Nginx cannot connect to the socket, check the service Group, UMask, and the socket ownership:

ls -l /run/gunicorn-myproject/gunicorn.sock

Gunicorn runs but Nginx returns 502

This usually means:

  • wrong proxy_pass
  • wrong socket path
  • permission issue on the socket
  • Gunicorn crashed after initial startup

Explanation

systemd is preferred over nohup, screen, or cron @reboot because it provides real service supervision. You get controlled restarts, boot integration, and logs in one place. That makes failures easier to detect and deployments easier to standardize.

Restart policies improve reliability because a transient application crash does not require manual intervention. Restart=on-failure is a reasonable default for Gunicorn in production.

Using RuntimeDirectory= also makes Unix socket setups reliable across reboots. Without it, a socket path under /run can disappear after restart because /run is temporary.

journald helps because you can inspect recent logs with a single command instead of searching ad hoc log files. That is especially useful when a deployment fails and the service exits before the reverse proxy can serve traffic.

When to automate this

If you deploy multiple Django apps or repeat this setup across environments, this manual process becomes a good candidate for a reusable template or script. The service file, environment file layout, and restart or health-check commands are usually consistent. A small deploy script can also restart Gunicorn, verify status, and switch back to a previous release path when application-level checks fail.

Edge cases / notes

  • Static and media files: systemd only manages Gunicorn. You still need a plan for static and media handling, usually through Nginx or object storage.
  • Proxy headers: if TLS terminates at Nginx or Caddy, configure Django to trust the proxy’s HTTPS header correctly, typically with SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https'), and only when that header is set by your reverse proxy.
  • CSRF over HTTPS: if your deployment uses HTTPS and a reverse proxy, make sure your Django settings include the correct CSRF_TRUSTED_ORIGINS values for your production domains.
  • Timeouts: if requests are long-running, review Gunicorn worker settings carefully instead of relying on defaults.
  • Multiple apps on one server: use separate service names, sockets, and preferably separate users where practical.
  • systemd socket units: socket activation exists, but it is usually unnecessary for standard Django deployments and adds complexity.
  • Reboot test: after enabling the service, reboot once during a maintenance window and confirm the app comes back automatically.

To place this setup in context, read Django Deployment Checklist for Production.
For the full web stack, continue with Deploy Django with Gunicorn and Nginx on Ubuntu.
If you are deploying an ASGI app instead of WSGI, see Deploy Django ASGI with Uvicorn and Nginx.
If you want an alternative reverse proxy, read Deploy Django with Caddy and Automatic HTTPS.

FAQ

Should I use a Unix socket or a TCP port for Gunicorn with systemd?

Use a Unix socket when Nginx and Gunicorn run on the same host. Use a localhost TCP port when that fits your topology better. Do not bind Gunicorn to a public interface unless you have a specific reason.

Where should I store Django environment variables when using systemd?

Use an EnvironmentFile such as /etc/myproject.env with restrictive permissions. Avoid putting secrets directly into the service unit when possible.

Why does systemctl start fail even though Gunicorn works manually?

Usually because systemd is missing the same environment, working directory, or binary path you used in the shell. Compare the manual command with ExecStart, WorkingDirectory, and EnvironmentFile.

Do I need to restart Gunicorn after every Django deployment?

Usually yes. After code changes, migrations, or dependency updates, restart Gunicorn so workers load the new release. Run migrations and collect static files before the restart.

Can I run multiple Django apps with separate Gunicorn systemd services on one server?

Yes. Give each app its own service name, project path, socket or port, and environment file. Separate users are also a good idea when practical.

2026 · django-deployment.com - Django Deployment knowledge base