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:
- Run Gunicorn as a dedicated non-root user.
- Confirm Gunicorn starts manually from the correct virtualenv.
- Create a
systemdservice unit pointing at the project directory, environment file, and Gunicorn binary. - Bind Gunicorn to a Unix socket or localhost TCP port.
- If you use a Unix socket under
/run, configureRuntimeDirectory=so it is recreated on boot. - Reload
systemd, start the service, and enable it on boot. - Verify with
systemctl statusandjournalctl. - 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=Falseonly works if your project reads that environment variable and maps it to Django’sDEBUGsetting.- 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 rootGroup: set this so Nginx can access the socket if neededWorkingDirectory: should match the Django project locationEnvironmentFile: loads settings outside the unit fileRuntimeDirectory: creates/run/gunicorn-myprojecton boot, which is important because/runis cleared on rebootUMask=0007: helps keep the socket group-accessible while not making it world-accessibleExecStart: full path to Gunicorn in the virtualenvRestart=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 statusshowsactive (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:
- upload or switch code
- install dependencies if needed
- run migrations
- collect static files
- restart Gunicorn
- verify health
- 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:
- point your
currentsymlink back to the previous release - restart Gunicorn
- verify status and logs
- 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:
WorkingDirectorypoints 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:
systemdonly 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_ORIGINSvalues 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.
Internal links
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.