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.
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
- 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" - Move deploy-specific values to environment variables
Keep secrets and environment-specific endpoints out of source code.
Typical variables:bashexport 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. - 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 - Load the selected config in WSGI
UseFLASK_CONFIGas the runtime selector.python# wsgi.py import os from myapp import create_app app = create_app(os.environ.get("FLASK_CONFIG", "production")) - Set safe defaults only for development
Do not define fallback production secrets or production database URLs in Python code.
Good:pythonSQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///dev.db")
Bad:pythonSQLALCHEMY_DATABASE_URI = os.environ.get( "DATABASE_URL", "postgresql://prod-user:prod-pass@prod-db/prod" ) - Make production config explicit
Production must disable debug and set security-related flags.pythonclass 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. - Use per-environment env files only for local convenience
Example local files:bash.env.dev .env.staging .env.prod
Example content:bashFLASK_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. - Set the environment in systemd
For server deployments, defineFLASK_CONFIGand required variables in the unit or anEnvironmentFile.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:bashsudo 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). - Set the environment in Docker or Compose
PassFLASK_CONFIGand secrets through container runtime variables.yamlservices: web: image: myapp:latest environment: FLASK_CONFIG: production SECRET_KEY: ${SECRET_KEY} DATABASE_URL: ${DATABASE_URL} ports: - "8000:8000"
Validate effective config:bashdocker compose config docker exec -it <container_name> /bin/sh -c 'printenv | sort | egrep "FLASK_CONFIG|DATABASE_URL|SECRET_KEY"' - Validate the loaded config before deploy
Confirm the application is using the expected environment and settings.bashpython -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'))" - 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. - Avoid deprecated
FLASK_ENVassumptions
Use your own selector such asFLASK_CONFIG. Treat that variable as the single source of truth for environment selection. - Add a startup confirmation log
Log only the environment name, never secrets.pythonimport 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 - Document the config selection flow
Common Causes
- Production runs with development settings →
FLASK_CONFIGis unset or defaults to development → setFLASK_CONFIG=productionin systemd, Docker, or deployment variables. - Wrong database used after deploy →
DATABASE_URLis 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=Trueand verify proxy/HTTPS behavior.
Debugging Section
Check runtime environment variables first:
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:
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:
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:
gunicorn --check-config 'wsgi:app'
Inspect containers:
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_CONFIGmatches the target environmentDEBUGisFalseoutside developmentDATABASE_URLpoints to the correct database- startup logs show the expected environment
- systemd or container config includes the intended variables
- Gunicorn loads
wsgi:appwithout import/config errors
If the app starts but requests fail upstream, also check Fix Flask 502 Bad Gateway (Step-by-Step Guide).
Checklist
-
FLASK_CONFIGis 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
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Environment Variables and Secrets Setup
- Flask Production Config Basics
- Flask Production Checklist (Everything You Must Do)
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.