Reference Guide Detailed deployment notes with production context and concrete examples.

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

bash
# 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

  1. Identify how the app is started
    Confirm whether Flask runs under systemd, Docker, Docker Compose, or another process manager.
    bash
    ps 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.
  2. Print the active service definition
    Inspect the live unit and all overrides.
    bash
    sudo 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.
  3. Put variables in the actual production startup path
    For systemd-managed Gunicorn, define variables directly in the unit or in an env file.
    bash
    sudo 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 use EnvironmentFile=.
  4. Use valid EnvironmentFile syntax
    systemd expects plain KEY=value lines.
    Correct:
    env
    SECRET_KEY=supersecret
    DATABASE_URL=postgresql://user:pass@127.0.0.1/appdb
    APP_ENV_MARKER=prod-check
    

    Incorrect:
    • export SECRET_KEY=supersecret
    • SECRET_KEY = supersecret
    • shell functions or command substitutions

    Validate the file:
    bash
    sudo grep -n -v '^#' /etc/default/myflaskapp
    sudo awk -F= 'NF<2 {print NR ": bad line -> " $0}' /etc/default/myflaskapp
    
  5. Store secrets outside the project directory
    Keep production secrets outside the repo.
    bash
    sudo 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
    
  6. Verify WorkingDirectory
    If your application relies on relative paths or python-dotenv, the service working directory must match the app location.
    bash
    sudo systemctl show gunicorn --property=WorkingDirectory
    

    Example:
    ini
    [Service]
    WorkingDirectory=/srv/myflaskapp
    

    A wrong working directory commonly causes .env discovery to fail silently.
  7. Check Flask config code
    Make sure the application reads from os.environ or a validated config object.
    Example:
    python
    import os
    
    SECRET_KEY = os.environ["SECRET_KEY"]
    DATABASE_URL = os.environ["DATABASE_URL"]
    

    Avoid hidden fallbacks like:
    python
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-value")
    

    In production, defaults can mask broken environment loading.
  8. Handle python-dotenv intentionally
    If you use python-dotenv, do not assume it will load automatically in production. Prefer systemd or container-defined environment variables.
    Example explicit load:
    python
    from dotenv import load_dotenv
    load_dotenv()
    

    If production depends on this, confirm:
    • the package is installed
    • the .env file exists
    • the service WorkingDirectory points to the expected project root
  9. Reload and restart after changes
    For systemd:
    bash
    sudo systemctl daemon-reload
    sudo systemctl restart gunicorn
    sudo systemctl status gunicorn --no-pager
    

    For Docker Compose:
    bash
    docker compose config
    docker compose up -d --force-recreate
    
  10. Validate with a safe test variable
    Add a non-secret marker such as:
    env
    APP_ENV_MARKER=prod-check
    

    Then restart and confirm the app can read it through logs or a temporary diagnostic endpoint.
    Example temporary check:
    python
    import os
    print("APP_ENV_MARKER present:", os.environ.get("APP_ENV_MARKER"))
    
  11. Inspect logs for config failures
    Look for:
    • KeyError
    • RuntimeError
    • config validation errors
    • import errors during startup
    bash
    sudo journalctl -u gunicorn -n 200 --no-pager
    sudo journalctl -u gunicorn -f
    
  12. 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 with Environment= or EnvironmentFile= in the service.
  • The wrong systemd unit was edited → changes do not affect the running app → verify the exact service name with systemctl status and systemctl cat.
  • EnvironmentFile syntax is invalid → systemd skips or misreads values → use plain KEY=value lines without export or shell syntax.
  • WorkingDirectory is wrongpython-dotenv or relative config loading misses the .env file → set the correct project directory in the service.
  • The service was restarted without daemon-reload after unit changes → new variables are not applied → run systemctl daemon-reload and 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_file or Compose environment entries are missing or overridden → container starts without expected values → inspect docker compose config and container Env output.
  • 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:

bash
sudo systemctl status gunicorn --no-pager
sudo systemctl cat gunicorn
sudo systemctl show gunicorn --property=Environment,User,Group,WorkingDirectory

Check logs:

bash
sudo journalctl -u gunicorn -n 200 --no-pager
sudo journalctl -u gunicorn -f

Confirm the running process and service name:

bash
ps aux | grep gunicorn
systemctl list-units --type=service | grep -i gunicorn

Validate env file contents and syntax:

bash
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:

bash
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
  • KeyError or 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.
  • EnvironmentFile path exists and uses valid KEY=value syntax.
  • WorkingDirectory points to the correct application path.
  • Gunicorn or the container has been restarted after env changes.
  • Logs no longer show missing config or KeyError exceptions.
  • The app can read a temporary non-secret test variable after restart.
  • Secrets are stored outside the repo and with restricted permissions.

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.