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

Flask Environment Variables and Secrets Setup

If you're trying to configure Flask environment variables and secrets for production, this guide shows you how to set them up safely and load them reliably with systemd and Gunicorn. The goal is to keep secrets out of source code and ensure your app reads the correct production settings on every deploy.

Quick Fix / Quick Setup

Create a protected environment file, point your systemd service at it, then restart Gunicorn.

bash
sudo mkdir -p /etc/myflaskapp
sudo tee /etc/myflaskapp/myflaskapp.env > /dev/null <<'EOF'
FLASK_ENV=production
SECRET_KEY=replace-with-long-random-value
DATABASE_URL=postgresql://user:password@127.0.0.1:5432/dbname
EOF
sudo chmod 600 /etc/myflaskapp/myflaskapp.env

sudo sed -i '/^EnvironmentFile=/d' /etc/systemd/system/myflaskapp.service
sudo tee /etc/systemd/system/myflaskapp.service > /dev/null <<'EOF'
[Unit]
Description=Gunicorn for myflaskapp
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myflaskapp
EnvironmentFile=/etc/myflaskapp/myflaskapp.env
ExecStart=/var/www/myflaskapp/venv/bin/gunicorn -w 3 -b unix:/run/myflaskapp.sock wsgi:app
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl restart myflaskapp
sudo systemctl status myflaskapp --no-pager
sudo systemctl show myflaskapp --property=Environment

Use an EnvironmentFile for production instead of exporting variables manually in a shell. Store the file outside the project directory, restrict permissions, and restart the service after changes.

What’s Happening

Flask reads configuration from process environment variables at startup. In production, Gunicorn usually runs under systemd, so variables exported in your interactive shell are not automatically available to the service. Secrets fail to load when they are defined in the wrong place, loaded too late, or blocked by file permission or service configuration issues.

Step-by-Step Guide

  1. Decide which values must come from environment variables.
    Typical production values:
    • SECRET_KEY
    • DATABASE_URL
    • mail credentials
    • API tokens
    • Flask config mode
    • feature flags
  2. Remove hardcoded secrets from Flask config.
    Read values with os.environ or os.getenv.
    python
    from os import environ
    
    SECRET_KEY = environ["SECRET_KEY"]
    SQLALCHEMY_DATABASE_URI = environ["DATABASE_URL"]
    

    If missing values should fail fast, use environ[...]. If optional, use getenv.
  3. Create a dedicated environment file outside the repository.
    bash
    sudo mkdir -p /etc/myflaskapp
    sudo nano /etc/myflaskapp/myflaskapp.env
    

    Add:
    dotenv
    FLASK_ENV=production
    SECRET_KEY=replace-with-long-random-value
    DATABASE_URL=postgresql://user:password@127.0.0.1:5432/dbname
    
  4. Restrict permissions on the secrets file.
    bash
    sudo chown root:root /etc/myflaskapp/myflaskapp.env
    sudo chmod 600 /etc/myflaskapp/myflaskapp.env
    
  5. Configure systemd to load the environment file.
    Edit /etc/systemd/system/myflaskapp.service:
    ini
    [Unit]
    Description=Gunicorn for myflaskapp
    After=network.target
    
    [Service]
    User=www-data
    Group=www-data
    WorkingDirectory=/var/www/myflaskapp
    EnvironmentFile=/etc/myflaskapp/myflaskapp.env
    ExecStart=/var/www/myflaskapp/venv/bin/gunicorn -w 3 -b unix:/run/myflaskapp.sock wsgi:app
    Restart=always
    
    [Install]
    WantedBy=multi-user.target
    

    Ensure:
    • WorkingDirectory matches your app path
    • ExecStart points to the correct virtualenv
    • the socket or bind target matches your Nginx config
  6. Reload systemd and restart the service.
    bash
    sudo systemctl daemon-reload
    sudo systemctl restart myflaskapp
    sudo systemctl status myflaskapp --no-pager
    
  7. Verify the running service sees the variables.
    bash
    sudo systemctl show myflaskapp --property=Environment
    sudo cat /proc/$(pgrep -f 'gunicorn.*myflaskapp' | head -n1)/environ | tr '\0' '\n' | grep -E 'FLASK_ENV|DATABASE_URL|SECRET_KEY'
    
  8. If you use python-dotenv, keep it for development only unless explicitly needed in production.
    For local development, .env can be fine. For production, prefer systemd EnvironmentFile or a dedicated secret manager. Do not assume Gunicorn will read .env automatically.
  9. Load config before initializing extensions.
    If you use an application factory, ensure environment-backed config is available before initializing extensions.
    Example order:
    python
    def create_app():
        app = Flask(__name__)
        app.config.from_object("config.ProductionConfig")
    
        db.init_app(app)
        mail.init_app(app)
        cache.init_app(app)
    
        return app
    
  10. Rotate secrets safely.
    • edit the env file
    • restart Gunicorn
    • validate app health
    • remove old credentials only after confirming the new ones work
  11. For containers, keep the same variable names.
    If you deploy with Docker or Compose, pass variables through container environment settings or secrets and keep names aligned with Flask config.

Common Causes

  • Variables exported in a shell only → systemd does not inherit them → define EnvironmentFile= or Environment= in the service unit.
  • Wrong variable names in Flask config → app looks for SECRET_KEY but file defines SECRET or APP_SECRET → align names exactly.
  • Environment file not readable by the service → permission denied or wrong path → set correct ownership, chmod 600, and verify the path in systemd.
  • Config loads too late in the app lifecycle → extensions initialize before env-backed settings are applied → load config before creating database, mail, cache, or login integrations.
  • Invalid env file syntax → quotes, spaces, or export statements break parsing → keep one KEY=value pair per line without shell-specific syntax.
  • Service not reloaded after changes → old values remain in memory → run daemon-reload when the unit changes and restart the service after env changes.
  • Using .env in production without explicit loading → file exists but Flask never reads it → either load dotenv before app creation or use systemd EnvironmentFile instead.
  • Secrets committed to Git → deployment works but creates a security risk → rotate exposed credentials and remove them from repository history if needed.

Debugging Section

Check service configuration, the running process environment, and startup logs.

Commands

bash
sudo systemctl cat myflaskapp
sudo systemctl show myflaskapp --property=Environment
sudo systemctl status myflaskapp --no-pager
sudo journalctl -u myflaskapp -n 100 --no-pager
sudo grep -n 'EnvironmentFile' /etc/systemd/system/myflaskapp.service
sudo ls -l /etc/myflaskapp/myflaskapp.env
sudo cat /proc/$(pgrep -f 'gunicorn.*myflaskapp' | head -n1)/environ | tr '\0' '\n'
sudo -u www-data env | sort
python3 -c "import os; print(os.getenv('SECRET_KEY')); print(os.getenv('DATABASE_URL'))"
sudo systemd-analyze verify /etc/systemd/system/myflaskapp.service

What to look for

  • EnvironmentFile= is present in the active unit
  • systemd shows the expected environment variables
  • Gunicorn starts without KeyError or config import failures
  • the env file path exists and permissions are correct
  • variable names in Flask code exactly match variable names in the env file
  • no invalid env file formatting such as export KEY=value

If the service fails completely, check Flask Gunicorn Service Failed to Start. If variables still do not appear inside the service, use Flask Environment Variables Not Loading in Production.

Checklist

  • Secrets are not hardcoded in the Flask repository.
  • Production variables are stored in a dedicated file or secret store outside the app codebase.
  • EnvironmentFile is present in the systemd service.
  • File permissions on the secrets file are restricted.
  • Gunicorn restarts successfully after config changes.
  • The running process shows the expected environment variables.
  • Flask connects successfully to the database and external services using those variables.
  • Secret rotation procedure is documented and tested.

For broader deployment validation, review Flask Production Checklist (Everything You Must Do).

FAQ

Q: Should I store .env inside the project directory in production?
A: Prefer a root-owned file outside the repository, such as /etc/myflaskapp/myflaskapp.env.

Q: Does Nginx need these environment variables?
A: Usually no. Gunicorn and the Flask app need them. Nginx mainly proxies requests.

Q: Why do variables work in my shell but not in production?
A: systemd services do not inherit your interactive shell exports unless you define them in the service configuration.

Q: Can I use python-dotenv in production?
A: You can, but systemd EnvironmentFile or a dedicated secret manager is more predictable for server deployments.

Q: When do I need to restart the app?
A: After changing environment variables or secrets, restart the Gunicorn service so Flask reloads config at startup.

Final Takeaway

Production Flask apps should load secrets from the process environment, not from source code. On a VPS, the most reliable pattern is a locked-down env file referenced by systemd and consumed by Gunicorn at startup. If variables are missing, debug from the running service context, not from your shell.