Flask Production Config Basics
If you're preparing a Flask app for production or trying to fix risky default settings, this guide shows you how to set up core production configuration step-by-step. The outcome is a Flask app that runs with production-safe defaults, externalized secrets, and predictable behavior behind a WSGI server.
Quick Fix / Quick Setup
Use this as the minimum production baseline:
# 1) Set required environment variables
export FLASK_ENV=production
export SECRET_KEY='change-this-to-a-long-random-value'
export DATABASE_URL='postgresql://user:pass@127.0.0.1:5432/appdb'
# 2) Minimal production config example
cat > config.py <<'EOF'
import os
class ProductionConfig:
DEBUG = False
TESTING = False
SECRET_KEY = os.environ['SECRET_KEY']
SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']
SQLALCHEMY_TRACK_MODIFICATIONS = False
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
PREFERRED_URL_SCHEME = 'https'
EOF
Load the config in your app factory:
app.config.from_object('config.ProductionConfig')
Run the app with Gunicorn, not flask run:
gunicorn -w 3 -b 127.0.0.1:8000 'wsgi:app'
This covers the minimum production baseline: no debug mode, no hardcoded secrets, secure cookies, and a real WSGI server. Adjust the database URL and app import path for your project.
What’s Happening
Flask defaults are development-friendly, not production-safe. Most production configuration issues come from leaving debug mode enabled, hardcoding secrets, using SQLite beyond development, or starting the app with flask run. A production setup should separate code from secrets and load explicit settings for the runtime environment.
Step-by-Step Guide
- Create a dedicated production config class
Add a config module with explicit production settings:python# config.py import os from datetime import timedelta class ProductionConfig: DEBUG = False TESTING = False SECRET_KEY = os.environ['SECRET_KEY'] SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL'] SQLALCHEMY_TRACK_MODIFICATIONS = False SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax' PREFERRED_URL_SCHEME = 'https' MAX_CONTENT_LENGTH = 16 * 1024 * 1024 PERMANENT_SESSION_LIFETIME = timedelta(hours=12) - Move secrets and environment-specific settings out of source code
Do not hardcode secrets, credentials, or production URLs.bashexport SECRET_KEY='replace-with-a-random-value' export DATABASE_URL='postgresql://user:pass@127.0.0.1:5432/appdb'
Typical values to externalize:SECRET_KEYDATABASE_URL- mail credentials
- third-party API keys
- storage credentials
- Load the production config explicitly in the app factory
Example app factory:python# app/__init__.py from flask import Flask def create_app(): app = Flask(__name__) app.config.from_object('config.ProductionConfig') @app.get("/health") def health(): return {"status": "ok"}, 200 return app - Create a WSGI entry point
Gunicorn should import a stable application object.python# wsgi.py from app import create_app app = create_app() - Set secure cookie behavior
For HTTPS deployments, keep cookies restricted:pythonSESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax'
Use'Strict'only if your login and cross-site flows allow it. - Use a production database
Replace SQLite with PostgreSQL or another production database for deployed multi-user applications.
Example:bashexport DATABASE_URL='postgresql://appuser:strongpassword@127.0.0.1:5432/appdb'
If you are still using SQLite from development, replace it before traffic increases. - Avoid
.env-only production loading
For production, prefer runtime-managed environment injection.
Examplesystemdservice:ini[Unit] Description=Gunicorn for Flask app After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/srv/yourapp Environment="SECRET_KEY=replace-with-real-secret" Environment="DATABASE_URL=postgresql://appuser:strongpassword@127.0.0.1:5432/appdb" ExecStart=/srv/yourapp/venv/bin/gunicorn -w 3 -b 127.0.0.1:8000 wsgi:app Restart=always [Install] WantedBy=multi-user.target - Run the app with Gunicorn
Start the app behind a real WSGI server:bashgunicorn -w 3 -b 127.0.0.1:8000 'wsgi:app'
For a full reverse-proxy deployment, continue with Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide). - Validate that Flask loaded the correct config
Confirm values from the actual runtime:bashpython -c "from app import create_app; app=create_app(); print('DEBUG=', app.config.get('DEBUG')); print('DB=', app.config.get('SQLALCHEMY_DATABASE_URI')); print('COOKIE_SECURE=', app.config.get('SESSION_COOKIE_SECURE'))"
Expected output:DEBUG= False- production database URL present
COOKIE_SECURE= True
- Restart the service after config changes
If using systemd:
sudo systemctl daemon-reload
sudo systemctl restart yourapp
sudo systemctl status yourapp --no-pager
- Verify response locally and through the proxy
Check Gunicorn directly:
curl -I http://127.0.0.1:8000
Check through the public endpoint:
curl -I https://yourdomain.com
- Confirm HTTPS and proxy behavior
If Flask generates http:// URLs behind Nginx, your proxy headers are incomplete. Validate your reverse proxy setup in Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide).
Common Causes
- DEBUG=True in production → exposes development behavior and can create security risk → set
DEBUG = Falsein the production config and stop using dev-only startup commands. - SECRET_KEY missing or hardcoded → sessions fail or secrets leak into source control → load it from environment variables.
- DATABASE_URL unset or invalid → app crashes at startup or fails on first database access → define and validate the production database URL.
- Environment variables not available to systemd or Gunicorn → config works in your shell but not in the service → define
Environment=or use a dedicated env file in the service definition. - Using
flask runinstead of Gunicorn → weak process management and poor production behavior → run Gunicorn behind Nginx. - HTTPS-related settings incomplete → incorrect redirects or insecure cookies → enable secure cookie settings and pass correct proxy headers.
- SQLite retained from development → file locking and reliability issues under concurrent traffic → migrate to PostgreSQL.
Debugging Section
Check environment visibility:
env | sort | egrep 'FLASK|SECRET_KEY|DATABASE_URL'
Inspect effective Flask config:
python -c "from app import create_app; app=create_app(); print('DEBUG=', app.config.get('DEBUG')); print('DB=', app.config.get('SQLALCHEMY_DATABASE_URI')); print('COOKIE_SECURE=', app.config.get('SESSION_COOKIE_SECURE'))"
Run Gunicorn with verbose startup logs:
gunicorn -w 3 -b 127.0.0.1:8000 'wsgi:app' --log-level debug
Inspect systemd service configuration:
systemctl cat yourapp
systemctl show yourapp --property=Environment
journalctl -u yourapp -n 100 --no-pager
Validate Nginx syntax:
sudo nginx -t
Test app reachability:
curl -I http://127.0.0.1:8000
curl -I https://yourdomain.com
Check whether the service process can see required variables:
python -c "import os; print(os.environ.get('SECRET_KEY')); print(os.environ.get('DATABASE_URL'))"
What to look for:
- missing
SECRET_KEY - empty or malformed
DATABASE_URL DEBUG=True- Gunicorn import errors
- Nginx proxy or TLS misconfiguration
systemdservice running with a different working directory or virtualenv
Checklist
-
DEBUGis disabled in production. -
TESTINGis disabled in production. -
SECRET_KEYis loaded from an environment variable. - Database configuration uses a production database URL.
-
SQLALCHEMY_TRACK_MODIFICATIONSis disabled if using Flask-SQLAlchemy. - Gunicorn is used instead of
flask run. - Secure cookie settings are enabled for HTTPS deployments.
- Production config is loaded explicitly by the app.
- Environment variables are available to the runtime process.
- The app restarts cleanly after config changes.
- Requests work through Nginx without debug pages or scheme issues.
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Environment Variables and Secrets Setup
- Flask Production Checklist (Everything You Must Do)
FAQ
What is the minimum production config for Flask?
Disable DEBUG and TESTING, load SECRET_KEY and database settings from environment variables, enable secure cookie settings, and run behind Gunicorn.
Should configuration live in code or environment variables?
Keep non-secret defaults in code and inject secrets or environment-specific values at runtime.
Can I use flask run in production?
No. Use Gunicorn or another production WSGI server.
Why are my environment variables missing only in production?
Your process manager may not load the same shell environment. Define variables in systemd, Docker, or your hosting runtime.
Do I need a separate config class for each environment?
Yes. Separate development, staging, and production settings to avoid accidental cross-environment behavior.
Final Takeaway
Production Flask config should be explicit, environment-driven, and safe by default. If you disable debug mode, externalize secrets, use Gunicorn, and verify runtime environment loading, most deployment configuration problems are avoided before deployment.