Deployment
Flask applications can be deployed in various ways, from simple single-server setups to complex cloud-native architectures. This guide covers different deployment strategies and best practices.
Production Configuration
Environment Configuration
# config.py
import os
from datetime import timedelta
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///dev.db'
class ProductionConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'postgresql://user:pass@localhost/prod'
# Security settings
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
PERMANENT_SESSION_LIFETIME = timedelta(hours=1)
# Performance settings
SQLALCHEMY_POOL_SIZE = 20
SQLALCHEMY_POOL_TIMEOUT = 20
SQLALCHEMY_POOL_RECYCLE = -1
SQLALCHEMY_MAX_OVERFLOW = 0
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}
Application Factory Pattern
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import config
db = SQLAlchemy()
migrate = Migrate()
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(config[config_name])
# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)
# Register blueprints
from app.main import bp as main_bp
app.register_blueprint(main_bp)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
# Error handlers
@app.errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('errors/500.html'), 500
return app
WSGI Servers
Gunicorn
# Installation
pip install gunicorn
# Basic usage
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"
# With configuration file
gunicorn -c gunicorn.conf.py "app:create_app()"
Gunicorn Configuration
# gunicorn.conf.py
import multiprocessing
# Server socket
bind = "0.0.0.0:8000"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 5
# Restart workers after this many requests
max_requests = 1000
max_requests_jitter = 50
# Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process naming
proc_name = 'flask_app'
# Server mechanics
preload_app = True
daemon = False
pidfile = '/tmp/gunicorn.pid'
user = 'www-data'
group = 'www-data'
tmp_upload_dir = None
# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
uWSGI
# Installation
pip install uwsgi
# Basic usage
uwsgi --http :8000 --module app:create_app() --callable app
# With ini file
uwsgi --ini uwsgi.ini
uWSGI Configuration
# uwsgi.ini
[uwsgi]
module = app:create_app()
callable = app
master = true
processes = 4
threads = 2
socket = /tmp/uwsgi.sock
chmod-socket = 666
vacuum = true
die-on-term = true
Web Servers
Nginx Configuration
# /etc/nginx/sites-available/flask_app
server {
listen 80;
server_name your-domain.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL configuration
ssl_certificate /path/to/ssl/cert.pem;
ssl_certificate_key /path/to/ssl/private.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
# Static files
location /static {
alias /path/to/app/static;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Media files
location /media {
alias /path/to/app/media;
expires 1y;
}
# Application
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Apache Configuration
# /etc/apache2/sites-available/flask_app.conf
<VirtualHost *:80>
ServerName your-domain.com
Redirect permanent / https://your-domain.com/
</VirtualHost>
<VirtualHost *:443>
ServerName your-domain.com
# SSL configuration
SSLEngine on
SSLCertificateFile /path/to/ssl/cert.pem
SSLCertificateKeyFile /path/to/ssl/private.key
# Static files
Alias /static /path/to/app/static
<Directory /path/to/app/static>
Require all granted
</Directory>
# Proxy to application
ProxyPreserveHost On
ProxyPass /static !
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
# Security headers
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Frame-Options DENY
Header always set X-Content-Type-Options nosniff
</VirtualHost>
Docker Deployment
Dockerfile
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK \
CMD curl -f http://localhost:8000/health || exit 1
# Start application
CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:create_app()"]
Docker Compose
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- '8000:8000'
environment:
- FLASK_ENV=production
- DATABASE_URL=postgresql://postgres:password@db:5432/flask_app
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
- redis
volumes:
- ./static:/app/static
- ./media:/app/media
restart: unless-stopped
db:
image: postgres:13
environment:
- POSTGRES_DB=flask_app
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:6-alpine
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- '80:80'
- '443:443'
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./ssl:/etc/nginx/ssl
- ./static:/var/www/static
depends_on:
- web
restart: unless-stopped
volumes:
postgres_data:
Multi-stage Docker Build
# Multi-stage Dockerfile for smaller production image
FROM python:3.11-slim as builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y \
gcc \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Production stage
FROM python:3.11-slim
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy Python packages from builder stage
COPY /root/.local /root/.local
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Make sure scripts in .local are usable
ENV PATH=/root/.local/bin:$PATH
EXPOSE 8000
HEALTHCHECK \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:create_app()"]
Cloud Deployment
Heroku Deployment
# Install Heroku CLI
# Create Procfile
echo "web: gunicorn app:create_app()" > Procfile
# Create runtime.txt (optional)
echo "python-3.11.0" > runtime.txt
# Initialize git and Heroku
git init
heroku create your-app-name
# Set environment variables
heroku config:set SECRET_KEY=your-secret-key
heroku config:set FLASK_ENV=production
# Add PostgreSQL addon
heroku addons:create heroku-postgresql:hobby-dev
# Deploy
git add .
git commit -m "Initial deployment"
git push heroku main
# Run migrations
heroku run flask db upgrade
AWS Elastic Beanstalk
# application.py (entry point for EB)
from app import create_app
application = create_app('production')
if __name__ == "__main__":
application.run()
# .ebextensions/01_flask.config
option_settings:
aws:elasticbeanstalk:container:python:
WSGIPath: application.py
aws:elasticbeanstalk:application:environment:
FLASK_ENV: production
PYTHONPATH: /opt/python/current/app
Google Cloud Run
# cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/flask-app', '.']
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/flask-app']
- name: 'gcr.io/cloud-builders/gcloud'
args:
- 'run'
- 'deploy'
- 'flask-app'
- '--image'
- 'gcr.io/$PROJECT_ID/flask-app'
- '--platform'
- 'managed'
- '--region'
- 'us-central1'
Environment Variables
Environment Configuration
# .env (development)
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=dev-secret-key
DATABASE_URL=sqlite:///dev.db
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
Production Environment Variables
# Production environment variables
export FLASK_ENV=production
export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
export DATABASE_URL=postgresql://user:pass@localhost/prod
export REDIS_URL=redis://localhost:6379/0
export MAIL_SERVER=smtp.sendgrid.net
export MAIL_PORT=587
export MAIL_USE_TLS=True
export MAIL_USERNAME=apikey
export MAIL_PASSWORD=your-sendgrid-api-key
Database Deployment
PostgreSQL Setup
# Install PostgreSQL
sudo apt-get install postgresql postgresql-contrib
# Create database and user
sudo -u postgres psql
CREATE DATABASE flask_app;
CREATE USER flask_user WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE flask_app TO flask_user;
\q
# Run migrations
export DATABASE_URL=postgresql://flask_user:secure_password@localhost/flask_app
flask db upgrade
Database Connection Pooling
# config.py
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
# Connection pooling
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_size': 20,
'pool_recycle': 3600,
'pool_pre_ping': True,
'max_overflow': 0
}
Monitoring and Logging
Logging Configuration
# app/logging_config.py
import logging
import os
from logging.handlers import RotatingFileHandler, SMTPHandler
def setup_logging(app):
if not app.debug and not app.testing:
# Email errors to admins
if app.config.get('MAIL_SERVER'):
auth = None
if app.config.get('MAIL_USERNAME') or app.config.get('MAIL_PASSWORD'):
auth = (app.config.get('MAIL_USERNAME'),
app.config.get('MAIL_PASSWORD'))
secure = None
if app.config.get('MAIL_USE_TLS'):
secure = ()
mail_handler = SMTPHandler(
mailhost=(app.config.get('MAIL_SERVER'), app.config.get('MAIL_PORT')),
fromaddr='no-reply@' + app.config.get('MAIL_SERVER'),
toaddrs=app.config.get('ADMINS', []),
subject='Flask App Failure',
credentials=auth,
secure=secure
)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
# Log to file
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/app.log',
maxBytes=10240000,
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)
app.logger.setLevel(logging.INFO)
app.logger.info('Flask application startup')
Health Check Endpoint
# app/main/routes.py
from flask import jsonify
from app.main import bp
from app import db
@bp.route('/health')
def health_check():
try:
# Check database connection
db.session.execute('SELECT 1')
return jsonify({
'status': 'healthy',
'database': 'connected'
}), 200
except Exception as e:
return jsonify({
'status': 'unhealthy',
'database': 'disconnected',
'error': str(e)
}), 503
SSL/HTTPS Setup
Let's Encrypt with Certbot
# Install certbot
sudo apt-get install certbot python3-certbot-nginx
# Obtain certificate
sudo certbot --nginx -d your-domain.com
# Auto-renewal (add to crontab)
0 12 * * * /usr/bin/certbot renew --quiet
SSL Configuration in Flask
# For development with self-signed certificate
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, ssl_context='adhoc')
# For production with proper certificates
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000,
ssl_context=('/path/to/cert.pem', '/path/to/key.pem'))
Performance Optimization
Caching Setup
# app/__init__.py
from flask_caching import Cache
cache = Cache()
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(config[config_name])
# Configure caching
app.config['CACHE_TYPE'] = 'redis'
app.config['CACHE_REDIS_URL'] = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
cache.init_app(app)
return app
# Usage in routes
from app import cache
@bp.route('/expensive-operation')
@cache.cached(timeout=300) # Cache for 5 minutes
def expensive_operation():
# Expensive computation
return result
Static File Optimization
# Serve static files with CDN in production
class ProductionConfig(Config):
# Use CDN for static files
STATIC_URL = 'https://cdn.your-domain.com/static/'
# In templates
{% if config.STATIC_URL %}
<link rel="stylesheet" href="{{ config.STATIC_URL }}css/style.css">
{% else %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% endif %}
Backup and Recovery
Database Backup Script
#!/bin/bash
# backup.sh
DB_NAME="flask_app"
DB_USER="flask_user"
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup
pg_dump -U $DB_USER -h localhost $DB_NAME > $BACKUP_DIR/backup_$DATE.sql
# Keep only last 7 days of backups
find $BACKUP_DIR -name "backup_*.sql" -mtime +7 -delete
echo "Backup completed: backup_$DATE.sql"
Automated Backup with Cron
# Add to crontab
0 2 * * * /path/to/backup.sh >> /var/log/backup.log 2>&1
Best Practices
Deployment Checklist
# 1. Security settings
DEBUG = False
SECRET_KEY = os.environ.get('SECRET_KEY') # Strong, random key
SESSION_COOKIE_SECURE = True # HTTPS only
SESSION_COOKIE_HTTPONLY = True
# 2. Database configuration
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 3. Error handling
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('errors/500.html'), 500
# 4. Logging
import logging
logging.basicConfig(level=logging.INFO)
# 5. Environment variables
# Never hardcode sensitive information
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
# 6. Static files
# Use CDN or proper web server for static files
# 7. Database migrations
# Always run migrations in production
flask db upgrade
# 8. SSL/HTTPS
# Always use HTTPS in production
# 9. Monitoring
# Set up health checks and monitoring
# 10. Backups
# Implement regular database backups
Zero-Downtime Deployment
#!/bin/bash
# deploy.sh - Blue-green deployment script
# Build new version
docker build -t app:new .
# Start new container
docker run -d --name app-new -p 8001:8000 app:new
# Health check
until curl -f http://localhost:8001/health; do
echo "Waiting for new version to be ready..."
sleep 5
done
# Switch traffic (update load balancer)
# Update nginx upstream or load balancer config
# Stop old container
docker stop app-old
docker rm app-old
# Rename new container
docker rename app-new app-old
echo "Deployment completed successfully"