Introduction
Deploying Flask to production requires careful attention to security, performance, reliability, and monitoring. This guide covers essential best practices and configurations.
Security
Disable Debug Mode
The most critical security requirement:
# config.py
import os
class Config :
DEBUG = False
TESTING = False
class DevelopmentConfig ( Config ):
DEBUG = True
class ProductionConfig ( Config ):
DEBUG = False
Never run with DEBUG = True in production. Debug mode exposes sensitive information and allows arbitrary code execution through the debugger.
Secret Key Management
Use a strong, random secret key and never commit it to version control:
import os
import secrets
class Config :
# Generate a secret key: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY = os.environ.get( 'SECRET_KEY' ) or secrets.token_hex( 32 )
Generate a secure key:
python -c "import secrets; print(secrets.token_hex(32))"
Set via environment variable:
export SECRET_KEY = 'your-generated-secret-key-here'
gunicorn -w 4 'myapp:app'
Running Without Root Privileges
Never run WSGI servers as root. This would cause your application code to run with elevated privileges, creating serious security risks.
Create a dedicated user:
sudo useradd -m -s /bin/bash myapp
sudo su - myapp
Run as non-root user:
# As myapp user
gunicorn -w 4 -b 127.0.0.1:8000 'myapp:app'
For privileged ports (80, 443), use a reverse proxy or capabilities:
# Grant capability to bind to privileged ports (alternative to root)
sudo setcap 'cap_net_bind_service=+ep' /path/to/venv/bin/python
Proxy Configuration
When running behind a reverse proxy, configure Flask to trust proxy headers:
from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask( __name__ )
# Only use if behind a proxy!
# Set x_for to the number of proxies
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for = 1 , # Number of proxies setting X-Forwarded-For
x_proto = 1 , # Number of proxies setting X-Forwarded-Proto
x_host = 1 , # Number of proxies setting X-Forwarded-Host
x_prefix = 1 # Number of proxies setting X-Forwarded-Prefix
)
Security Risk: Only enable ProxyFix if your application is actually behind a proxy. Setting incorrect proxy counts can be a security vulnerability, as headers can be spoofed.
HTTPS/TLS Configuration
Always use HTTPS in production. Configure your reverse proxy (Nginx/Apache) to handle TLS:
Nginx with Let’s Encrypt:
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on ;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
proxy_set_header X-Forwarded-Proto $ scheme ;
proxy_set_header X-Forwarded-Host $ host ;
proxy_set_header X-Forwarded-Prefix /;
}
}
# Redirect HTTP to HTTPS
server {
listen 80 ;
server_name example.com;
return 301 https://$ server_name $ request_uri ;
}
Force HTTPS in Flask:
from flask import Flask, redirect, request
app = Flask( __name__ )
@app.before_request
def before_request ():
if not request.is_secure and app.config.get( 'FORCE_HTTPS' ):
url = request.url.replace( 'http://' , 'https://' , 1 )
return redirect(url, code = 301 )
Implement security headers to protect against common vulnerabilities:
from flask import Flask
app = Flask( __name__ )
@app.after_request
def set_security_headers ( response ):
response.headers[ 'X-Content-Type-Options' ] = 'nosniff'
response.headers[ 'X-Frame-Options' ] = 'SAMEORIGIN'
response.headers[ 'X-XSS-Protection' ] = '1; mode=block'
response.headers[ 'Strict-Transport-Security' ] = 'max-age=31536000; includeSubDomains'
response.headers[ 'Content-Security-Policy' ] = "default-src 'self'"
return response
Or use Flask-Talisman:
pip install flask-talisman
from flask import Flask
from flask_talisman import Talisman
app = Flask( __name__ )
Talisman(app)
Environment Configuration
Environment Variables
Use environment variables for configuration:
# config.py
import os
class Config :
SECRET_KEY = os.environ.get( 'SECRET_KEY' )
SQLALCHEMY_DATABASE_URI = os.environ.get( 'DATABASE_URL' )
REDIS_URL = os.environ.get( 'REDIS_URL' )
MAIL_SERVER = os.environ.get( 'MAIL_SERVER' )
MAIL_PORT = int (os.environ.get( 'MAIL_PORT' , 587 ))
MAIL_USE_TLS = os.environ.get( 'MAIL_USE_TLS' , 'true' ).lower() in [ 'true' , '1' , 't' ]
class ProductionConfig ( Config ):
DEBUG = False
TESTING = False
class DevelopmentConfig ( Config ):
DEBUG = True
TESTING = False
Use .env files (development only):
pip install python-dotenv
# app.py
from dotenv import load_dotenv
import os
load_dotenv() # Load .env file
app.config[ 'SECRET_KEY' ] = os.environ.get( 'SECRET_KEY' )
Production environment variables:
# /etc/systemd/system/myapp.service
[Service]
Environment = "SECRET_KEY=your-secret-key"
Environment = "DATABASE_URL=postgresql://user:pass@localhost/db"
Environment = "FLASK_ENV=production"
Configuration Loading
# app.py
from flask import Flask
import os
app = Flask( __name__ )
# Load configuration
config_name = os.environ.get( 'FLASK_CONFIG' , 'production' )
app.config.from_object( f 'config. { config_name.capitalize() } Config' )
# Or from file
app.config.from_pyfile( '/etc/myapp/config.py' , silent = True )
Logging
Production Logging Configuration
import logging
from logging.handlers import RotatingFileHandler, SysLogHandler
import os
def configure_logging ( app ):
if not app.debug and not app.testing:
# Ensure log directory exists
if not os.path.exists( 'logs' ):
os.mkdir( 'logs' )
# File handler with rotation
file_handler = RotatingFileHandler(
'logs/myapp.log' ,
maxBytes = 10240000 , # 10MB
backupCount = 10
)
file_handler.setFormatter(logging.Formatter(
' %(asctime)s %(levelname)s : %(message)s '
'[in %(pathname)s : %(lineno)d ]'
))
file_handler.setLevel(logging. INFO )
app.logger.addHandler(file_handler)
# Syslog handler for production
syslog_handler = SysLogHandler( address = '/dev/log' )
syslog_handler.setLevel(logging. WARNING )
app.logger.addHandler(syslog_handler)
app.logger.setLevel(logging. INFO )
app.logger.info( 'Application startup' )
configure_logging(app)
Structured Logging
For better log analysis:
pip install python-json-logger
from pythonjsonlogger import jsonlogger
import logging
logger = logging.getLogger()
logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
logger.addHandler(logHandler)
app.logger.info( 'User login' , extra = { 'user_id' : user.id, 'ip' : request.remote_addr})
Error Tracking
Integrate error tracking services:
Sentry:
pip install sentry-sdk[flask]
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init(
dsn = "your-sentry-dsn" ,
integrations = [FlaskIntegration()],
traces_sample_rate = 1.0 ,
environment = "production"
)
Worker Configuration
Gunicorn configuration:
# gunicorn.conf.py
import multiprocessing
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'sync'
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
timeout = 30
keepalive = 2
# Preload app for faster worker spawn
preload_app = True
Database Connection Pooling
from flask_sqlalchemy import SQLAlchemy
app.config[ 'SQLALCHEMY_POOL_SIZE' ] = 10
app.config[ 'SQLALCHEMY_POOL_RECYCLE' ] = 3600
app.config[ 'SQLALCHEMY_POOL_TIMEOUT' ] = 30
app.config[ 'SQLALCHEMY_MAX_OVERFLOW' ] = 20
db = SQLAlchemy(app)
Caching
Flask-Caching:
pip install flask-caching
from flask_caching import Cache
cache_config = {
'CACHE_TYPE' : 'RedisCache' ,
'CACHE_REDIS_URL' : os.environ.get( 'REDIS_URL' ),
'CACHE_DEFAULT_TIMEOUT' : 300
}
app.config.from_mapping(cache_config)
cache = Cache(app)
@app.route ( '/expensive-operation' )
@cache.cached ( timeout = 600 )
def expensive_operation ():
# This will be cached for 10 minutes
return perform_expensive_query()
Static File Serving
Let your reverse proxy serve static files instead of Flask for better performance.
Nginx configuration:
server {
listen 80 ;
server_name example.com;
# Serve static files directly
location /static {
alias /var/www/myapp/static;
expires 30d ;
add_header Cache-Control "public, immutable" ;
}
# Proxy dynamic requests to Flask
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
proxy_set_header X-Forwarded-Proto $ scheme ;
proxy_set_header X-Forwarded-Host $ host ;
}
}
Compression
Enable gzip in Nginx:
http {
gzip on ;
gzip_vary on ;
gzip_proxied any;
gzip_comp_level 6 ;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss;
}
Process Management
Systemd Service
Create a systemd service for automatic startup and management:
# /etc/systemd/system/myapp.service
[Unit]
Description =My Flask Application
After =network.target
[Service]
User =myapp
Group =myapp
WorkingDirectory =/home/myapp/myapp
Environment = "PATH=/home/myapp/myapp/.venv/bin"
Environment = "SECRET_KEY=your-secret-key"
Environment = "DATABASE_URL=postgresql://user:pass@localhost/db"
ExecStart =/home/myapp/myapp/.venv/bin/gunicorn \
-w 4 \
-b 127.0.0.1:8000 \
--access-logfile /var/log/myapp/access.log \
--error-logfile /var/log/myapp/error.log \
'myapp:create_app()'
Restart =always
RestartSec =10
[Install]
WantedBy =multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
View logs:
sudo journalctl -u myapp -f
Supervisor
Alternatively, use Supervisor:
sudo apt-get install supervisor
# /etc/supervisor/conf.d/myapp.conf
[program:myapp]
command =/home/myapp/myapp/.venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 'myapp:create_app()'
directory =/home/myapp/myapp
user =myapp
autostart =true
autorestart =true
stopasgroup =true
killasgroup =true
stderr_logfile =/var/log/myapp/error.log
stdout_logfile =/var/log/myapp/access.log
environment = SECRET_KEY = "your-secret-key" , DATABASE_URL = "postgresql://user:pass@localhost/db"
Control:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start myapp
sudo supervisorctl status myapp
Monitoring
Health Check Endpoint
from flask import Flask, jsonify
import psutil
@app.route ( '/health' )
def health_check ():
return jsonify({
'status' : 'healthy' ,
'database' : check_database_connection(),
'cache' : check_cache_connection(),
}), 200
def check_database_connection ():
try :
db.session.execute( 'SELECT 1' )
return 'connected'
except Exception as e:
app.logger.error( f 'Database health check failed: { e } ' )
return 'disconnected'
Application Metrics
Prometheus with Flask:
pip install prometheus-flask-exporter
from flask import Flask
from prometheus_flask_exporter import PrometheusMetrics
app = Flask( __name__ )
metrics = PrometheusMetrics(app)
# Metrics available at /metrics endpoint
Uptime Monitoring
Use external monitoring services:
UptimeRobot - Free basic monitoring
Pingdom - Comprehensive monitoring
StatusCake - Performance monitoring
Datadog - Full-stack monitoring
Deployment Checklist
Common Production Issues
Causes:
WSGI server not running
Wrong socket/port configuration
Firewall blocking connection
Solutions: # Check if WSGI server is running
sudo systemctl status myapp
# Check if port is listening
sudo netstat -tlnp | grep :8000
# Check firewall
sudo ufw status
Database Connection Errors
Causes:
Connection pool exhausted
Database not accessible
Wrong credentials
Solutions: # Increase pool size
app.config[ 'SQLALCHEMY_POOL_SIZE' ] = 20
app.config[ 'SQLALCHEMY_MAX_OVERFLOW' ] = 40
# Add connection recycling
app.config[ 'SQLALCHEMY_POOL_RECYCLE' ] = 3600
Causes:
Long-running requests
Blocking operations
Resource exhaustion
Solutions: # Increase timeout
gunicorn --timeout 60 'myapp:app'
# Use async workers for long-polling
gunicorn -k gevent 'myapp:app'
# Move long tasks to background queue (Celery)
Causes:
Too many workers
Memory leaks
Large objects in memory
Solutions: # Restart workers after N requests
# gunicorn.conf.py
max_requests = 1000
max_requests_jitter = 50
# Monitor memory
import psutil
process = psutil.Process()
print ( f "Memory: { process.memory_info().rss / 1024 / 1024 } MB" )
Additional Resources
WSGI Servers Configure Gunicorn, uWSGI, and other WSGI servers
Deployment Overview Review deployment concepts and options
External Resources