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.

From env file to settings/etc/myapp.envKEY=value linessystemdparses at startGunicorn processinherits envos.environread by settingssystemd reads the file at service start; edits change nothing until the next restart.
  1. Inspect the running process's environment with /proc/<pid>/environ, not your shell.
  2. Check the unit really references the file: systemctl cat gunicorn.
  3. Use plain KEY=value lines: no export, no spaces around =, no $OTHER expansion.
  4. Remove a leading - from EnvironmentFile=-/path while debugging; it silently ignores a missing file.
  5. After editing the unit: systemctl daemon-reload, then restart. After editing only the file: restart (not reload).
  6. Load the same file when running manage.py by 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 from EnvironmentFile override Environment= 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"
EnvironmentFile is not a shell scriptResultKEY=value✓worksKEY="value with spaces"✓works, quotes removedexport KEY=value✕shell syntax: not reliably acceptedKEY = value✕invalid: no spaces around =KEY=${OTHER}!not expanded: literal $ textWindows line endings add an invisible \r to every valuedjango-deployment.com
journalctl -u gunicorn shows lines systemd ignored.

Common mistakes:

LineProblem
export DEBUG=Falseshell syntax; not reliably accepted by systemd, and your settings may not see it
DEBUG = Falsespaces around = break the assignment
DATABASE_URL=postgres://$DB_USER:...no variable expansion; the value contains the literal text $DB_USER
multi-line valuesnot 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
one character can hide the problem[Service]EnvironmentFile=-/etc/myapp.envUser=myapp1231Missing file silently ignored: service starts without secrets.2Read by systemd as root, before User= applies.3The app does not need read access to the file.Drop the dash so a missing file fails loudlydjango-deployment.com
Keep the file owned by root with mode 600.

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
what each change needsEdit the env filevalues onlyEdit the unit?then daemon-reloadsystemctl restartreload is not enoughNew processreads the file at startVerifysystemctl show -p Environment…The environment is fixed when the process startsdjango-deployment.com
A HUP reload keeps the master process and its old environment.
  • daemon-reload makes systemd re-read unit files. It does not restart services.
  • restart starts a new process that reads the environment file again.
  • reload (Gunicorn's graceful HUP) keeps the master process and its environment; new values are not picked up. Use restart for 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

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.

Keep going