Fix a Gunicorn systemd Service That Fails to Start
Fix a Gunicorn service for Django that systemd cannot start: status codes 203/EXEC, 200/CHDIR, 217/USER, environment files, socket directories, and restart loops.
Problem statement
After creating or editing a systemd unit for Gunicorn, the service will not start:
$ sudo systemctl start gunicorn
Job for gunicorn.service failed because the control process exited with error code.
See "systemctl status gunicorn.service" and "journalctl -xeu gunicorn.service" for details.
systemctl status shows a line such as:
Process: 4121 ExecStart=/srv/myapp/venv/bin/gunicorn ... (code=exited, status=203/EXEC)
Unlike Python tracebacks, systemd status codes such as 203/EXEC or 200/CHDIR describe failures that happen before Gunicorn runs any Python code: systemd could not set up the process. Other failures happen after Gunicorn starts (exit status 1 or 3 with a Python error). Reading which kind you have saves a lot of guesswork.
Quick answer
- Run
systemctl status gunicorn --no-pager -landjournalctl -u gunicorn -n 50 --no-pager. - Match the status:
203/EXEC:ExecStartpath wrong, not executable, or its interpreter missing200/CHDIR:WorkingDirectorydoes not exist or is not accessible to the user217/USER: theUser=orGroup=does not exist226/NAMESPACEorReadWritePathserrors: sandboxing options reference missing paths- failure mentioning
EnvironmentFile: the environment file is missing status=1orstatus=3with a traceback: Python or Gunicorn error at startup
- After editing the unit, run
sudo systemctl daemon-reloadbefore restarting. - Test the exact
ExecStartcommand manually as the service user. - Use
RuntimeDirectory=for socket directories under/run.
Step-by-step solution
1. Collect the details
systemctl status gunicorn --no-pager -l
sudo journalctl -u gunicorn -n 50 --no-pager
systemctl cat gunicorn
systemctl cat prints the unit exactly as systemd loaded it, including drop-in overrides from /etc/systemd/system/gunicorn.service.d/. Edits to a unit file are ignored until you run:
sudo systemctl daemon-reload
A frequent "nothing changes" moment is editing the unit and restarting without reloading; systemctl status then warns that the unit file changed on disk.
2. status=203/EXEC
systemd could not execute the program in ExecStart.
ls -l /srv/myapp/venv/bin/gunicorn
head -1 /srv/myapp/venv/bin/gunicorn
sudo -u myapp /srv/myapp/venv/bin/gunicorn --version
Causes:
- Relative or wrong path.
ExecStartneeds an absolute path to the executable.ExecStart=gunicorn ...relies on systemd's limited search path and fails when Gunicorn lives in a virtualenv. - Missing interpreter. The script's shebang points at a Python that no longer exists (the virtualenv was created with a Python that was removed or upgraded). Recreate the virtualenv.
- Not executable or on a
noexecmount. Checkmount | grep noexecfor the filesystem holding the virtualenv. - SELinux denying execution from a home directory on RHEL-family systems; check
ausearch -m avc -ts recent.
3. status=200/CHDIR
sudo -u myapp test -d /srv/myapp/app && echo "exists and accessible"
namei -l /srv/myapp/app
WorkingDirectory must exist and be traversable by the service user. A path under a home directory with mode 750 fails for another user, and a deploy that replaced a symlink target can leave WorkingDirectory pointing at a removed directory.
4. status=217/USER
id myapp
getent group www-data
The user or group in User= and Group= does not exist. Create the system user:
sudo adduser --system --group --home /srv/myapp myapp
5. EnvironmentFile failures
If the environment file is missing, systemd refuses to start the service and the journal names the file:
gunicorn.service: Failed to load environment files: No such file or directory
Fix the path or create the file. A leading - (EnvironmentFile=-/etc/myapp/env) makes a missing file acceptable, which hides mistakes; use it only for genuinely optional files.
Also check the file format: KEY=value per line, no export keyword. Lines systemd cannot parse are logged and ignored, which can later surface as missing variables in Django.
6. Socket directory problems
With a Unix socket bind:
[ERROR] Retrying in 1 second.
[ERROR] Can't connect to /run/gunicorn/gunicorn.sock
PermissionError: [Errno 13] Permission denied: '/run/gunicorn'
/run is a tmpfs recreated on boot, so directories created manually there disappear after a reboot, and the service user cannot create them itself. Let systemd create it:
[Service]
User=myapp
Group=www-data
RuntimeDirectory=gunicorn
ExecStart=/srv/myapp/venv/bin/gunicorn config.wsgi:application \
--bind unix:/run/gunicorn/gunicorn.sock
RuntimeDirectory=gunicorn creates /run/gunicorn owned by User and Group each time the service starts. A service that worked until the first reboot is the classic sign of a missing RuntimeDirectory.
If you use socket activation with a gunicorn.socket unit instead, the socket unit owns the path; check it with systemctl status gunicorn.socket.
7. Sandboxing options
Hardening options can prevent startup:
gunicorn.service: Failed to set up mount namespacing: /srv/myapp/media: No such file or directory
gunicorn.service: Failed at step NAMESPACE spawning /srv/myapp/venv/bin/gunicorn: No such file or directory
ReadWritePaths=, ReadOnlyPaths=, and similar options require the listed paths to exist (prefix a path with - to make it optional). Create the directories or correct the paths.
8. Gunicorn starts, then exits
If systemd starts Gunicorn but it exits with status 1 or 3, the journal contains Gunicorn and Python output:
gunicorn[5012]: ModuleNotFoundError: No module named 'config'
gunicorn[5012]: [ERROR] Worker (pid:5013) exited with code 3
gunicorn[5012]: [ERROR] Shutting down: Master
gunicorn[5012]: [ERROR] Reason: Worker failed to boot.
This is an application problem, not a systemd problem. Reproduce it with the same user, directory, and environment:
sudo -u myapp bash -c 'cd /srv/myapp/app && set -a && . /etc/myapp/env && set +a && \
/srv/myapp/venv/bin/gunicorn config.wsgi:application --bind 127.0.0.1:8001'
Common causes: wrong module path, missing environment variables, database unreachable at import time, or a syntax error in settings.
9. Restart loops and start limits
With Restart=always or Restart=on-failure, a failing service restarts repeatedly until systemd's start rate limit is reached:
gunicorn.service: Start request repeated too quickly.
gunicorn.service: Failed with result 'exit-code'.
After fixing the cause, clear the failed state:
sudo systemctl reset-failed gunicorn
sudo systemctl start gunicorn
A RestartSec=2 or higher avoids tight loops that flood the journal.
10. A known-good unit for reference
[Unit]
Description=Gunicorn for myapp
After=network.target
[Service]
Type=notify
User=myapp
Group=www-data
WorkingDirectory=/srv/myapp/app
EnvironmentFile=/etc/myapp/env
RuntimeDirectory=gunicorn
ExecStart=/srv/myapp/venv/bin/gunicorn config.wsgi:application \
--bind unix:/run/gunicorn/gunicorn.sock \
--workers 3 \
--access-logfile - \
--error-logfile -
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=30
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
Gunicorn supports systemd's notify protocol, so Type=notify lets systemd know when Gunicorn is actually ready. If your Gunicorn version or environment does not send the notification, systemd times out the start; switch to Type=simple in that case.
KillMode=mixed sends SIGTERM to the master only, letting Gunicorn shut workers down gracefully, then kills any leftovers after the timeout.
11. Verify
sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn
systemctl is-active gunicorn
ls -l /run/gunicorn/gunicorn.sock
curl -sI --unix-socket /run/gunicorn/gunicorn.sock http://localhost/ -H "Host: www.example.com" | head -1
sudo reboot # optional: confirm it comes back after a reboot
Verification check:
is-activeprintsactive- the socket exists with the expected owner and group
- a request through the socket returns an HTTP status
- after a reboot, the service starts automatically
Explanation
systemd starts a service in stages: resolve the user and group, set up namespaces and sandboxing, load environment files, change into the working directory, and execute the command. Each stage has its own failure code, reported before your application runs. Once ExecStart executes, failures come from Gunicorn and Django and appear as tracebacks in the journal. Distinguishing the two tells you whether to fix the unit file or the application.
Edge cases / notes
Works with systemctl start, fails at boot
Dependencies may not be ready at boot, for example a network filesystem or a database on the same host. Add After= and Wants= for the required units, such as postgresql.service or network-online.target.
User services
Units under ~/.config/systemd/user/ run in the user's service manager, which stops when the user logs out unless lingering is enabled (loginctl enable-linger). Production services belong in /etc/systemd/system/.
Validating units before deploying them
systemd ships a linter that catches many unit file mistakes before you restart a production service:
sudo systemd-analyze verify /etc/systemd/system/gunicorn.service
It reports unknown directives, missing executables, and invalid values. Combine it with a dry run of the command as the service user, and you catch most failure codes from this guide without taking the running service down.
For hardening, systemd-analyze security gunicorn.service scores the unit's sandboxing. Add options gradually and restart after each one, so if the service stops starting you know which option caused it.
Overrides instead of editing packaged units
If a unit file comes from a package or a configuration management tool, put local changes in a drop-in:
sudo systemctl edit gunicorn
This opens an override file under /etc/systemd/system/gunicorn.service.d/override.conf. To replace list-type settings such as ExecStart, clear them first:
[Service]
ExecStart=
ExecStart=/srv/myapp/venv/bin/gunicorn config.wsgi:application --bind unix:/run/gunicorn/gunicorn.sock --workers 4
Without the empty ExecStart= line, systemd reports that a service has more than one ExecStart= setting and refuses to start it (unless the type is oneshot).
Watching a start attempt live
Open the journal in one terminal and start the service in another:
sudo journalctl -u gunicorn -f
sudo systemctl start gunicorn
You see every message in order, including messages from systemd itself about each setup stage, which is often clearer than the summarized status output after the fact.
Common mistakes
- Editing the unit file without
systemctl daemon-reload. - Creating the socket directory manually under
/runinstead of usingRuntimeDirectory=. - Using a relative path in
ExecStart. - Adding
-toEnvironmentFile=and hiding a wrong path.
Next steps
- Review a full service setup in How to Configure systemd for Gunicorn and Django.
- For import errors after start, see Fix ModuleNotFoundError When Starting Django with Gunicorn.
- If Nginx cannot use the socket, see Fix Gunicorn Socket Permission Denied.
- For missing settings, see Fix ImproperlyConfigured: SECRET_KEY Must Not Be Empty.
FAQ
What does status=203/EXEC mean for a Gunicorn service?
systemd could not execute the ExecStart program. The path is wrong, the file is not executable, or its Python interpreter no longer exists. Use the absolute path to Gunicorn in the virtualenv.
Why does my Gunicorn socket directory disappear after reboot?
/run is a temporary filesystem recreated at boot. Use RuntimeDirectory= in the unit so systemd creates the directory with correct ownership each time the service starts.
Do I need daemon-reload after changing a systemd unit?
Yes. systemd keeps the loaded unit in memory until you run sudo systemctl daemon-reload. Changes to environment files only need a service restart.
How do I fix "Start request repeated too quickly"?
Fix the underlying failure from the journal, then run sudo systemctl reset-failed gunicorn to clear the rate limit and start the service again.