Skip to main content

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)

Security Headers

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"
)

Performance Optimization

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

1

Configuration

  • Set DEBUG = False
  • Configure strong SECRET_KEY
  • Set up environment variables
  • Configure database connection pooling
  • Set up caching (Redis/Memcached)
2

Security

  • Enable HTTPS/TLS
  • Configure security headers
  • Set up ProxyFix middleware
  • Run as non-root user
  • Configure firewall rules
  • Set up fail2ban (optional)
3

Performance

  • Configure worker processes
  • Set up reverse proxy (Nginx/Apache)
  • Enable static file serving
  • Enable compression
  • Configure CDN (optional)
4

Reliability

  • Set up process manager (systemd/supervisor)
  • Configure logging
  • Set up error tracking (Sentry)
  • Create health check endpoints
  • Configure automated backups
5

Monitoring

  • Set up uptime monitoring
  • Configure metrics collection
  • Set up alerts
  • Configure log aggregation
  • Test disaster recovery

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
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

Build docs developers (and LLMs) love