Fix systemd EnvironmentFile Variables Not Reaching Django
Fix Django settings that miss variables from a systemd EnvironmentFile: file syntax rules, the '-' prefix, quoting, no variable expansion, daemon-reload versus restart, and checking a running process.
Problem statement
Your Gunicorn unit has EnvironmentFile=/etc/myapp.env, the file contains the right values, and yet Django fails with KeyError: 'DJANGO_SECRET_KEY', uses the wrong database, or behaves as if DEBUG were on. Management commands you run by hand behave differently again.
systemd's environment files look like shell scripts but are not: they have their own parsing rules, they are read at service start only, and changes to units need a reload while changes to the file need a restart. This guide checks what the running process actually received and fixes the common mistakes.
- Inspect the running process's environment with
/proc/<pid>/environ, not your shell. - Check the unit really references the file:
systemctl cat gunicorn. - Use plain
KEY=valuelines: noexport, no spaces around=, no$OTHERexpansion. - Remove a leading
-fromEnvironmentFile=-/pathwhile debugging; it silently ignores a missing file. - After editing the unit:
systemctl daemon-reload, then restart. After editing only the file: restart (not reload). - Load the same file when running
manage.pyby hand, or you are testing a different configuration.
Step-by-step solution
1. See what the process actually has
PID=$(systemctl show -p MainPID --value gunicorn)
sudo cat /proc/$PID/environ | tr '\0' '\n' | sort | sed 's/\(SECRET\|PASSWORD\|KEY\)=.*/\1=<redacted>/'
This is the environment Gunicorn started with. If a variable is missing or wrong here, the problem is in systemd; if it is correct here, the problem is in how settings read it.
systemctl show gunicorn -p Environment shows only Environment= lines from the unit, not values from EnvironmentFile, so it is not a reliable check.
2. Confirm the unit and its drop-ins
systemctl cat gunicorn
This prints the unit file plus every drop-in (/etc/systemd/system/gunicorn.service.d/*.conf). Look for:
EnvironmentFile=with the right path and without typos.- A drop-in that overrides settings you expect from the main file.
Environment=lines for the same variables. Values fromEnvironmentFileoverrideEnvironment=values in the unit.
3. Fix the file syntax
systemd accepts:
# /etc/myapp.env
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=8f3k...long-random...
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DATABASE_URL=postgres://myapp:p%40ss@10.0.0.5:5432/myapp
EMAIL_HOST_PASSWORD="value with spaces"
Common mistakes:
| Line | Problem |
|---|---|
export DEBUG=False | shell syntax; not reliably accepted by systemd, and your settings may not see it |
DEBUG = False | spaces around = break the assignment |
DATABASE_URL=postgres://$DB_USER:... | no variable expansion; the value contains the literal text $DB_USER |
| multi-line values | not supported; keep each variable on one line |
| Windows line endings | \r becomes part of the value; convert with dos2unix |
Quotes around a value are removed. Lines starting with # or ; are comments.
A single file can be shared with shell scripts if it sticks to this subset: shells can source KEY=value lines too (with set -a to export them).
4. Check the path and permissions
sudo ls -l /etc/myapp.env
sudo systemctl status gunicorn --no-pager | grep -i environment
If the file is missing, systemd refuses to start the service with Failed to load environment files, unless the path is prefixed with -, in which case it starts without those variables. Remove the - while debugging so a missing file is loud.
systemd reads the file as root before switching to User=, so root:root with mode 600 or root:myapp with 640 both work. The file does not need to be readable by the app user, which is a good property for secrets.
5. Reload and restart correctly
sudo systemctl daemon-reload # after editing the unit or a drop-in
sudo systemctl restart gunicorn # after editing the unit OR the environment file
daemon-reloadmakes systemd re-read unit files. It does not restart services.restartstarts a new process that reads the environment file again.reload(Gunicorn's gracefulHUP) keeps the master process and its environment; new values are not picked up. Userestartfor environment changes.
Apply the same to every unit that uses the file: Celery workers, Beat, and timers' service units.
6. Run management commands with the same environment
A manual python manage.py migrate in an SSH session does not see the service's environment. Load the file explicitly:
sudo -u myapp bash -c 'set -a; . /etc/myapp.env; set +a; cd /srv/myapp/current && .venv/bin/python manage.py migrate'
Or run the command as a transient unit with the same settings:
sudo systemd-run --uid=myapp --pipe --wait \
--property=EnvironmentFile=/etc/myapp.env \
--working-directory=/srv/myapp/current \
/srv/myapp/current/.venv/bin/python manage.py showmigrations
A small wrapper script (/usr/local/bin/myapp-manage) that does this saves everyone from forgetting it.
7. Check how settings read variables
DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"
String comparisons are exact: DJANGO_DEBUG=true, 1, or "False" with stray quotes behave differently than intended. Parse booleans deliberately:
def env_bool(name, default=False):
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
Verification check:
sudo systemctl restart gunicorn
PID=$(systemctl show -p MainPID --value gunicorn)
sudo cat /proc/$PID/environ | tr '\0' '\n' | grep -c -E "^(DJANGO_|DATABASE_URL)"
sudo -u myapp bash -c 'set -a; . /etc/myapp.env; set +a; cd /srv/myapp/current && .venv/bin/python manage.py check --deploy'
Explanation
When systemd starts a service, it builds the environment from defaults, Environment= lines, and EnvironmentFile= contents, then executes ExecStart with that environment. The file is parsed by systemd's own simple rules, not by a shell, and only at that moment. Gunicorn and its workers inherit the environment and keep it for their lifetime. Everything in this guide follows from those three facts: the syntax is systemd's, the timing is service start, and the result lives in the process.
Edge cases / notes
Secrets managers
If secrets come from a secrets manager at start-up, a wrapper script can fetch them and exec Gunicorn, or systemd's LoadCredential= can pass them as files under $CREDENTIALS_DIRECTORY. Both avoid long-lived plain-text files.
Docker is different
Inside containers, environment comes from the image and the container runtime, not systemd. See the Docker Compose guide for that case.
When to automate this
Generate the environment file from your secret store during deploys, set its permissions in the same step, restart every consuming unit, and verify /proc/<pid>/environ contains the expected keys (without printing values).
Next steps
- Review unit structure with How to Configure systemd for Gunicorn and Django.
- If the service will not start at all, see Fix a Gunicorn systemd Service That Fails to Start.
- Organise variables with Configure Django Environment Variables for Production.
- If the secret key is missing, see Fix ImproperlyConfigured: SECRET_KEY Must Not Be Empty.
FAQ
Why aren't my EnvironmentFile changes applied?
The environment is read at service start. Restart the service; a reload or daemon-reload alone does not apply new values.
Can I use export in a systemd EnvironmentFile?
Avoid it. Use plain KEY=value lines, which both systemd and shells understand.
Does systemd expand $VARIABLES in an EnvironmentFile?
No. Values are taken literally. Write full values, or build composite values in your settings code.
Why does manage.py behave differently from the running site?
Your SSH session does not have the service's environment. Load the same environment file before running commands.