Skip to main content

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 --interval=30s --timeout=30s --start-period=5s --retries=3 \
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 --from=builder /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 --interval=30s --timeout=30s --start-period=5s --retries=3 \
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"