Flask Environment Variables Not Loading in Production
If your Flask app works locally but production environment variables are missing or incorrect, this guide shows you how to fix the loading path step-by-step. The goal is to make your app read the correct variables reliably under the actual process manager running production.
Quick Fix / Quick Setup
# 1) Check what systemd is actually passing to Gunicorn
sudo systemctl show gunicorn --property=Environment
sudo systemctl cat gunicorn
# 2) Add or correct Environment entries in the service override
sudo systemctl edit gunicorn
# Example override:
# [Service]
# Environment="FLASK_ENV=production"
# Environment="SECRET_KEY=change-me"
# Environment="DATABASE_URL=postgresql://user:pass@127.0.0.1/dbname"
# EnvironmentFile=/etc/default/myflaskapp
# 3) If using an EnvironmentFile, verify format
sudo grep -v '^#' /etc/default/myflaskapp
# 4) Reload and restart
sudo systemctl daemon-reload
sudo systemctl restart gunicorn
sudo systemctl status gunicorn --no-pager
# 5) Confirm inside the running service logs
sudo journalctl -u gunicorn -n 100 --no-pager
In production, Flask does not automatically inherit the same shell environment you use interactively. Most failures come from setting variables in .bashrc, .profile, or a local .env file that Gunicorn or systemd never reads.
What’s Happening
Production Flask processes usually run under systemd, Docker, or another supervisor, not your interactive shell. Variables exported manually in a terminal, shell profile, or SSH session are usually unavailable to Gunicorn workers. Nginx does not provide application environment variables to Flask; Gunicorn or the container runtime must pass them explicitly.
Step-by-Step Guide
- Identify how the app is started
Confirm whether Flask runs under systemd, Docker, Docker Compose, or another process manager.bashps aux | grep gunicorn sudo systemctl status gunicorn --no-pager systemctl list-units --type=service | grep -i gunicorn
If the app runs in Docker instead of systemd, fix the environment in container configuration, not in your shell. - Print the active service definition
Inspect the live unit and all overrides.bashsudo systemctl cat gunicorn sudo systemctl show gunicorn --property=Environment,User,Group,WorkingDirectory
Check for:Environment=EnvironmentFile=WorkingDirectory=User=ExecStart=
Make sure you are editing the correct service name. - Put variables in the actual production startup path
For systemd-managed Gunicorn, define variables directly in the unit or in an env file.bashsudo systemctl edit gunicorn
Example override:ini[Service] Environment="FLASK_ENV=production" Environment="SECRET_KEY=change-me" Environment="DATABASE_URL=postgresql://user:pass@127.0.0.1/dbname" EnvironmentFile=/etc/default/myflaskapp
If you already use/etc/default/myflaskapp, keep secrets there and useEnvironmentFile=. - Use valid
EnvironmentFilesyntax
systemd expects plainKEY=valuelines.
Correct:envSECRET_KEY=supersecret DATABASE_URL=postgresql://user:pass@127.0.0.1/appdb APP_ENV_MARKER=prod-check
Incorrect:export SECRET_KEY=supersecretSECRET_KEY = supersecret- shell functions or command substitutions
Validate the file:bashsudo grep -n -v '^#' /etc/default/myflaskapp sudo awk -F= 'NF<2 {print NR ": bad line -> " $0}' /etc/default/myflaskapp - Store secrets outside the project directory
Keep production secrets outside the repo.bashsudo mkdir -p /etc/myflaskapp sudo cp /etc/default/myflaskapp /etc/myflaskapp/env sudo chmod 600 /etc/myflaskapp/env ls -l /etc/myflaskapp/env
Update the unit if needed:ini[Service] EnvironmentFile=/etc/myflaskapp/env - Verify
WorkingDirectory
If your application relies on relative paths orpython-dotenv, the service working directory must match the app location.bashsudo systemctl show gunicorn --property=WorkingDirectory
Example:ini[Service] WorkingDirectory=/srv/myflaskapp
A wrong working directory commonly causes.envdiscovery to fail silently. - Check Flask config code
Make sure the application reads fromos.environor a validated config object.
Example:pythonimport os SECRET_KEY = os.environ["SECRET_KEY"] DATABASE_URL = os.environ["DATABASE_URL"]
Avoid hidden fallbacks like:pythonSECRET_KEY = os.environ.get("SECRET_KEY", "dev-value")
In production, defaults can mask broken environment loading. - Handle
python-dotenvintentionally
If you usepython-dotenv, do not assume it will load automatically in production. Prefer systemd or container-defined environment variables.
Example explicit load:pythonfrom dotenv import load_dotenv load_dotenv()
If production depends on this, confirm:- the package is installed
- the
.envfile exists - the service
WorkingDirectorypoints to the expected project root
- Reload and restart after changes
For systemd:bashsudo systemctl daemon-reload sudo systemctl restart gunicorn sudo systemctl status gunicorn --no-pager
For Docker Compose:bashdocker compose config docker compose up -d --force-recreate - Validate with a safe test variable
Add a non-secret marker such as:envAPP_ENV_MARKER=prod-check
Then restart and confirm the app can read it through logs or a temporary diagnostic endpoint.
Example temporary check:pythonimport os print("APP_ENV_MARKER present:", os.environ.get("APP_ENV_MARKER")) - Inspect logs for config failures
Look for:KeyErrorRuntimeError- config validation errors
- import errors during startup
bashsudo journalctl -u gunicorn -n 200 --no-pager sudo journalctl -u gunicorn -f - Remove temporary diagnostics
After confirming correct loading:- remove test log statements
- remove temporary endpoints
- keep secrets only in the approved production source of truth
Common Causes
- Variables were added to
~/.bashrc,~/.profile, or a manual shell export → Gunicorn never sees them → define them withEnvironment=orEnvironmentFile=in the service. - The wrong systemd unit was edited → changes do not affect the running app → verify the exact service name with
systemctl statusandsystemctl cat. EnvironmentFilesyntax is invalid → systemd skips or misreads values → use plainKEY=valuelines withoutexportor shell syntax.WorkingDirectoryis wrong →python-dotenvor relative config loading misses the.envfile → set the correct project directory in the service.- The service was restarted without
daemon-reloadafter unit changes → new variables are not applied → runsystemctl daemon-reloadand restart. - Variables are defined in a deploy script but not persisted for the service → values disappear after the shell exits → store them in the service or container config.
- Docker
env_fileor Composeenvironmententries are missing or overridden → container starts without expected values → inspectdocker compose configand containerEnvoutput. - Application config code uses defaults or conditional loading that masks failures → missing variables go unnoticed until runtime → validate required keys at startup.
- The env file permissions are too restrictive or owned by the wrong user → service cannot read it → fix ownership and mode for the service user.
- A secret contains special characters and was copied with broken quoting or whitespace → parsing fails or the value is truncated → simplify formatting and test with a temporary marker variable.
Debugging Section
Check service status and the active unit:
sudo systemctl status gunicorn --no-pager
sudo systemctl cat gunicorn
sudo systemctl show gunicorn --property=Environment,User,Group,WorkingDirectory
Check logs:
sudo journalctl -u gunicorn -n 200 --no-pager
sudo journalctl -u gunicorn -f
Confirm the running process and service name:
ps aux | grep gunicorn
systemctl list-units --type=service | grep -i gunicorn
Validate env file contents and syntax:
sudo grep -n -v '^#' /etc/default/myflaskapp
sudo awk -F= 'NF<2 {print NR ": bad line -> " $0}' /etc/default/myflaskapp
ls -l /etc/default/myflaskapp
If using Docker:
docker compose config
docker inspect <container_name> | grep -A 50 '"Env"'
What to look for:
- missing
Environment=entries - wrong
EnvironmentFile=path - wrong
WorkingDirectory - unreadable env file
KeyErroror startup exceptions in Gunicorn logs- variables present in compose config but absent in the running container
Checklist
- The app is started by the runtime you expect: systemd, Docker, or Compose.
- Required variables are defined in the production startup configuration, not only in your shell profile.
-
EnvironmentFilepath exists and uses validKEY=valuesyntax. -
WorkingDirectorypoints to the correct application path. - Gunicorn or the container has been restarted after env changes.
- Logs no longer show missing config or
KeyErrorexceptions. - The app can read a temporary non-secret test variable after restart.
- Secrets are stored outside the repo and with restricted permissions.
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Environment Variables and Secrets Setup
- Flask Gunicorn Service Failed to Start
- Flask Production Checklist (Everything You Must Do)
FAQ
Q: Why are environment variables missing only in production?
A: Production usually runs under systemd or Docker, which does not inherit your interactive shell environment.
Q: Is .env enough for Flask production?
A: Not usually. Use systemd EnvironmentFile or container environment settings for predictable production loading.
Q: Do I need to restart Gunicorn after changing env vars?
A: Yes. Restart the service or recreate the container so the new environment is applied.
Q: Can Nginx set Flask environment variables?
A: No. Nginx is a reverse proxy; the application process manager must provide env vars.
Q: What is the safest place for production secrets?
A: A protected env file outside the repo or a managed secret store, referenced by the service runtime.
Final Takeaway
This issue is usually caused by defining variables in the wrong place. Put environment variables in the same runtime that starts Gunicorn or the container, reload that runtime, and verify the active environment directly instead of assuming your shell settings apply.