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

Flask Config Environments: Dev vs Staging vs Production

If you're trying to separate Flask development, staging, and production settings, this guide shows you how to structure environment-specific config step-by-step. The goal is to prevent unsafe defaults, avoid mixing secrets and databases, and make deployments predictable.

Quick Fix / Quick Setup

Create a central config module, load it through an app factory, and select the active environment with FLASK_CONFIG.

bash
mkdir -p /var/www/myapp
cd /var/www/myapp

cat > config.py <<'EOF'
import os

class BaseConfig:
    SECRET_KEY = os.environ.get('SECRET_KEY', 'change-me')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevelopmentConfig(BaseConfig):
    DEBUG = True
    TESTING = False
    ENV_NAME = 'development'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///dev.db')

class StagingConfig(BaseConfig):
    DEBUG = False
    TESTING = False
    ENV_NAME = 'staging'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

class ProductionConfig(BaseConfig):
    DEBUG = False
    TESTING = False
    ENV_NAME = 'production'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    SESSION_COOKIE_SECURE = True
    REMEMBER_COOKIE_SECURE = True
    PREFERRED_URL_SCHEME = 'https'
EOF

cat > wsgi.py <<'EOF'
import os
from myapp import create_app

app = create_app(os.environ.get('FLASK_CONFIG', 'production'))
EOF

export FLASK_CONFIG=development
export SECRET_KEY='dev-secret'
export DATABASE_URL='sqlite:///dev.db'
python -c "from wsgi import app; print(app.config['ENV_NAME'], app.config['DEBUG'])"

Use an app factory and load the config from FLASK_CONFIG. In production, set FLASK_CONFIG=production in systemd or your container environment instead of hardcoding values.

What’s Happening

Flask apps often break across environments because one settings file is reused everywhere. Development needs fast iteration and verbose errors, staging should safely mirror production, and production must disable debug and load real secrets. A clean config split prevents using the wrong database, exposing debug mode, or deploying local defaults to live traffic.

Step-by-Step Guide

  1. Create a central config module
    Put shared settings in a base class and override environment-specific settings in separate subclasses.
    python
    # config.py
    import os
    
    class BaseConfig:
        SECRET_KEY = os.environ.get("SECRET_KEY")
        SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    class DevelopmentConfig(BaseConfig):
        DEBUG = True
        TESTING = False
        ENV_NAME = "development"
        SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///dev.db")
    
    class StagingConfig(BaseConfig):
        DEBUG = False
        TESTING = False
        ENV_NAME = "staging"
        SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
    
    class ProductionConfig(BaseConfig):
        DEBUG = False
        TESTING = False
        ENV_NAME = "production"
        SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
        SESSION_COOKIE_SECURE = True
        REMEMBER_COOKIE_SECURE = True
        PREFERRED_URL_SCHEME = "https"
    
  2. Move deploy-specific values to environment variables
    Keep secrets and environment-specific endpoints out of source code.
    Typical variables:
    bash
    export FLASK_CONFIG=development
    export SECRET_KEY='replace-me'
    export DATABASE_URL='postgresql://user:pass@localhost/dbname'
    export REDIS_URL='redis://127.0.0.1:6379/0'
    export SMTP_HOST='smtp.example.com'
    

    For secret handling patterns, see Flask Environment Variables and Secrets Setup.
  3. Implement an app factory
    Map the selected environment string to the correct config class.
    python
    # myapp/__init__.py
    from flask import Flask
    from config import DevelopmentConfig, StagingConfig, ProductionConfig
    
    CONFIG_MAP = {
        "development": DevelopmentConfig,
        "staging": StagingConfig,
        "production": ProductionConfig,
    }
    
    def create_app(config_name="production"):
        app = Flask(__name__)
        config_class = CONFIG_MAP.get(config_name)
        if config_class is None:
            raise RuntimeError(f"Invalid FLASK_CONFIG value: {config_name}")
    
        app.config.from_object(config_class)
    
        @app.get("/health")
        def health():
            return {"status": "ok", "env": app.config.get("ENV_NAME")}
    
        return app
    
  4. Load the selected config in WSGI
    Use FLASK_CONFIG as the runtime selector.
    python
    # wsgi.py
    import os
    from myapp import create_app
    
    app = create_app(os.environ.get("FLASK_CONFIG", "production"))
    
  5. Set safe defaults only for development
    Do not define fallback production secrets or production database URLs in Python code.
    Good:
    python
    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///dev.db")
    

    Bad:
    python
    SQLALCHEMY_DATABASE_URI = os.environ.get(
        "DATABASE_URL",
        "postgresql://prod-user:prod-pass@prod-db/prod"
    )
    
  6. Make production config explicit
    Production must disable debug and set security-related flags.
    python
    class ProductionConfig(BaseConfig):
        DEBUG = False
        TESTING = False
        ENV_NAME = "production"
        SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
        SESSION_COOKIE_SECURE = True
        REMEMBER_COOKIE_SECURE = True
        PREFERRED_URL_SCHEME = "https"
    

    For production-safe defaults, see Flask Production Config Basics.
  7. Use per-environment env files only for local convenience
    Example local files:
    bash
    .env.dev
    .env.staging
    .env.prod
    

    Example content:
    bash
    FLASK_CONFIG=staging
    SECRET_KEY=staging-secret
    DATABASE_URL=postgresql://staging_user:staging_pass@127.0.0.1/staging_db
    

    Do not commit real secrets to source control.
  8. Set the environment in systemd
    For server deployments, define FLASK_CONFIG and required variables in the unit or an EnvironmentFile.
    ini
    # /etc/systemd/system/myapp.service
    [Unit]
    Description=Gunicorn for myapp
    After=network.target
    
    [Service]
    User=www-data
    Group=www-data
    WorkingDirectory=/var/www/myapp
    Environment=FLASK_CONFIG=production
    Environment=SECRET_KEY=replace-with-real-secret
    Environment=DATABASE_URL=postgresql://myapp:strongpass@127.0.0.1/myapp
    ExecStart=/var/www/myapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app
    
    [Install]
    WantedBy=multi-user.target
    

    Apply changes:
    bash
    sudo systemctl daemon-reload
    sudo systemctl restart myapp
    sudo systemctl status myapp
    

    For the full service stack, see Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide).
  9. Set the environment in Docker or Compose
    Pass FLASK_CONFIG and secrets through container runtime variables.
    yaml
    services:
      web:
        image: myapp:latest
        environment:
          FLASK_CONFIG: production
          SECRET_KEY: ${SECRET_KEY}
          DATABASE_URL: ${DATABASE_URL}
        ports:
          - "8000:8000"
    

    Validate effective config:
    bash
    docker compose config
    docker exec -it <container_name> /bin/sh -c 'printenv | sort | egrep "FLASK_CONFIG|DATABASE_URL|SECRET_KEY"'
    
  10. Validate the loaded config before deploy
    Confirm the application is using the expected environment and settings.
    bash
    python -c "from myapp import create_app; app=create_app('production'); print(app.config.get('ENV_NAME'), app.config.get('DEBUG'), app.config.get('SQLALCHEMY_DATABASE_URI'))"
    
  11. Keep staging close to production
    Staging should use:
    • the same WSGI server class
    • similar Nginx behavior
    • the same database engine type
    • HTTPS where possible
    • similar middleware and extension setup

    Do not treat staging like development.
  12. Avoid deprecated FLASK_ENV assumptions
    Use your own selector such as FLASK_CONFIG. Treat that variable as the single source of truth for environment selection.
  13. Add a startup confirmation log
    Log only the environment name, never secrets.
    python
    import logging
    
    def create_app(config_name="production"):
        app = Flask(__name__)
        app.config.from_object(CONFIG_MAP[config_name])
        app.logger.setLevel(logging.INFO)
        app.logger.info("Starting app with config: %s", app.config.get("ENV_NAME"))
        return app
    
  14. Document the config selection flow

Common Causes

  • Production runs with development settingsFLASK_CONFIG is unset or defaults to development → set FLASK_CONFIG=production in systemd, Docker, or deployment variables.
  • Wrong database used after deployDATABASE_URL is shared or hardcoded across environments → define a unique database URL per environment and verify it at startup.
  • Secrets missing in staging or production → environment variables are not loaded by systemd or the container runtime → use EnvironmentFile, env_file, or secret injection and restart the service.
  • App behaves differently between staging and production → staging does not mirror production closely enough → align WSGI server, proxy headers, HTTPS behavior, and extension settings.
  • Debug toolbar or verbose error pages visible in production → debug-related extensions are enabled globally → load them only in DevelopmentConfig.
  • Config changes do not apply after deploy → Gunicorn or systemd still uses old environment values → run daemon-reload, restart services, and verify active environment variables.
  • Cookies fail only in production → secure cookie settings are missing or inconsistent with HTTPS termination → set SESSION_COOKIE_SECURE=True and verify proxy/HTTPS behavior.

Debugging Section

Check runtime environment variables first:

bash
printenv | sort | egrep 'FLASK_CONFIG|SECRET_KEY|DATABASE_URL|REDIS_URL'
python -c "import os; print(os.environ.get('FLASK_CONFIG'), os.environ.get('DATABASE_URL'))"

Validate loaded Flask config directly:

bash
python -c "from myapp import create_app; app=create_app('production'); print(app.config.get('ENV_NAME'), app.config.get('DEBUG'), app.config.get('SQLALCHEMY_DATABASE_URI'))"

Inspect systemd configuration:

bash
systemctl cat myapp
systemctl show myapp --property=Environment
sudo systemctl daemon-reload && sudo systemctl restart myapp
sudo journalctl -u myapp -n 100 --no-pager

Validate Gunicorn startup config:

bash
gunicorn --check-config 'wsgi:app'

Inspect containers:

bash
docker compose config
docker exec -it <container_name> /bin/sh -c 'printenv | sort | egrep "FLASK_CONFIG|DATABASE_URL|SECRET_KEY"'

What to look for:

  • FLASK_CONFIG matches the target environment
  • DEBUG is False outside development
  • DATABASE_URL points to the correct database
  • startup logs show the expected environment
  • systemd or container config includes the intended variables
  • Gunicorn loads wsgi:app without import/config errors

If the app starts but requests fail upstream, also check Fix Flask 502 Bad Gateway (Step-by-Step Guide).

Checklist

  • FLASK_CONFIG is set explicitly in each environment
  • Production does not run with DEBUG=True
  • Each environment uses a different database or isolated schema
  • Secrets are loaded from environment variables or a secret manager
  • Staging behavior matches production closely enough to catch deploy issues
  • A startup check confirms the active config before serving traffic
  • systemd or container definitions include the required variables
  • No production secrets are hardcoded in source files

FAQ

Q: What is the minimum environment split for Flask?
A: Use at least development and production. Add staging if you deploy changes before production.

Q: Should staging and production share secrets?
A: No. Use separate secrets and usually separate databases or isolated schemas.

Q: Can I store config in Python classes and env vars together?
A: Yes. Keep structure in Python classes and inject sensitive or deploy-specific values through environment variables.

Q: How do I confirm the correct config loaded?
A: Print a non-sensitive startup log showing the selected environment and inspect service environment variables.

Q: Should staging use DEBUG=True?
A: No. Keep staging close to production and debug through logs and monitoring.

Q: Should I use FLASK_ENV?
A: Prefer your own config selector such as FLASK_CONFIG and explicit environment variables.

Q: Where should production secrets live?
A: In a systemd EnvironmentFile, container secrets, or a managed secret store, not in source control.

Final Takeaway

Use one config entry point, separate classes per environment, and environment variables for all secrets and deploy-specific values. The key requirement is explicit selection of development, staging, or production at runtime and validation before traffic reaches the app.