15 KiB
15 KiB
Flask Deployment Configuration
Complete production deployment setup for Flask applications.
Usage
# Build Docker image
docker build -t myapp .
# Run with Docker Compose
docker-compose up -d
# Deploy to cloud
gunicorn --bind 0.0.0.0:8000 app:app
Production Configuration
# config.py
import os
from urllib.parse import quote_plus
class ProductionConfig:
"""Production configuration."""
# Security
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = False
TESTING = False
# Database
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
f"postgresql://{os.environ.get('DB_USER')}:{quote_plus(os.environ.get('DB_PASSWORD'))}@" \
f"{os.environ.get('DB_HOST')}:{os.environ.get('DB_PORT', '5432')}/{os.environ.get('DB_NAME')}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_size': 10,
'max_overflow': 20,
'pool_recycle': 3600,
'pool_pre_ping': True
}
# Security Headers
SECURITY_HEADERS = {
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
}
# Session
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
PERMANENT_SESSION_LIFETIME = 3600 # 1 hour
# Cache
CACHE_TYPE = 'redis'
CACHE_REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
CACHE_DEFAULT_TIMEOUT = 300
# Rate Limiting
RATELIMIT_STORAGE_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/1')
RATELIMIT_DEFAULT = '100/hour'
# Logging
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')
LOG_FILE = os.environ.get('LOG_FILE', '/var/log/app/app.log')
# File Upload
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', '/var/uploads')
# Email
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', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')
WSGI Configuration
# wsgi.py
import os
from app import create_app
# Get environment
config_name = os.environ.get('FLASK_ENV', 'production')
app = create_app(config_name)
if __name__ == "__main__":
app.run()
Gunicorn Configuration
# gunicorn.conf.py
import multiprocessing
import os
# Server socket
bind = f"0.0.0.0:{os.environ.get('PORT', 8000)}"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'sync'
worker_connections = 1000
timeout = 30
keepalive = 60
max_requests = 1000
max_requests_jitter = 100
# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
# Logging
accesslog = '-'
errorlog = '-'
loglevel = os.environ.get('LOG_LEVEL', 'info').lower()
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
daemon = False
pidfile = '/tmp/gunicorn.pid'
user = os.environ.get('USER', 'www-data')
group = os.environ.get('GROUP', 'www-data')
tmp_upload_dir = None
# SSL
keyfile = os.environ.get('SSL_KEYFILE')
certfile = os.environ.get('SSL_CERTFILE')
Docker Configuration
# Dockerfile
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create app user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Set work directory
WORKDIR /app
# Install Python dependencies
COPY requirements/production.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create necessary directories
RUN mkdir -p /var/log/app /var/uploads && \
chown -R appuser:appuser /app /var/log/app /var/uploads
# Switch to non-root user
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
# Run application
CMD ["gunicorn", "--config", "gunicorn.conf.py", "wsgi: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/myapp
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
- redis
volumes:
- uploads:/var/uploads
- logs:/var/log/app
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- uploads:/var/uploads:ro
depends_on:
- web
restart: unless-stopped
volumes:
postgres_data:
redis_data:
uploads:
logs:
Nginx Configuration
# nginx.conf
events {
worker_connections 1024;
}
http {
upstream app {
server web:8000;
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# File upload size
client_max_body_size 16M;
# Static files
location /static/ {
alias /var/uploads/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /uploads/ {
alias /var/uploads/;
expires 1h;
}
# API rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app;
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;
}
# Login rate limiting
location /auth/login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://app;
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;
}
# Main application
location / {
proxy_pass http://app;
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;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
}
Environment Variables
# .env.production
# Application
FLASK_ENV=production
SECRET_KEY=your-super-secret-key-here
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=user
DB_PASSWORD=password
# Redis
REDIS_URL=redis://localhost:6379/0
# Email
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
MAIL_DEFAULT_SENDER=your-email@gmail.com
# Logging
LOG_LEVEL=INFO
LOG_FILE=/var/log/app/app.log
# File Upload
UPLOAD_FOLDER=/var/uploads
# SSL (if using)
SSL_KEYFILE=/path/to/private.key
SSL_CERTFILE=/path/to/certificate.crt
Health Check Endpoint
# app/health.py
from flask import Blueprint, jsonify
from app.extensions import db
from sqlalchemy import text
import redis
import os
health_bp = Blueprint('health', __name__)
@health_bp.route('/health')
def health_check():
"""Application health check."""
checks = {
'status': 'healthy',
'database': check_database(),
'redis': check_redis(),
'disk_space': check_disk_space()
}
# Determine overall status
if all(check['status'] == 'ok' for check in checks.values() if isinstance(check, dict)):
status_code = 200
else:
status_code = 503
checks['status'] = 'unhealthy'
return jsonify(checks), status_code
def check_database():
"""Check database connectivity."""
try:
db.session.execute(text('SELECT 1'))
return {'status': 'ok', 'message': 'Database connection successful'}
except Exception as e:
return {'status': 'error', 'message': str(e)}
def check_redis():
"""Check Redis connectivity."""
try:
redis_url = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
r = redis.from_url(redis_url)
r.ping()
return {'status': 'ok', 'message': 'Redis connection successful'}
except Exception as e:
return {'status': 'error', 'message': str(e)}
def check_disk_space():
"""Check available disk space."""
try:
import shutil
total, used, free = shutil.disk_usage('/')
free_percent = (free / total) * 100
if free_percent > 10:
status = 'ok'
elif free_percent > 5:
status = 'warning'
else:
status = 'critical'
return {
'status': status,
'free_space_percent': round(free_percent, 2),
'free_space_gb': round(free / (1024**3), 2)
}
except Exception as e:
return {'status': 'error', 'message': str(e)}
Monitoring and Logging
# app/logging.py
import logging
import logging.handlers
import os
from flask import request, g
import time
def setup_logging(app):
"""Setup application logging."""
if not app.debug and not app.testing:
# File logging
if app.config.get('LOG_FILE'):
file_handler = logging.handlers.RotatingFileHandler(
app.config['LOG_FILE'],
maxBytes=10240000, # 10MB
backupCount=10
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(getattr(logging, app.config.get('LOG_LEVEL', 'INFO')))
app.logger.addHandler(file_handler)
# Console logging
if not app.logger.handlers:
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s'
))
stream_handler.setLevel(logging.INFO)
app.logger.addHandler(stream_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Application startup')
# Request timing middleware
@app.before_request
def before_request():
g.start_time = time.time()
@app.after_request
def after_request(response):
if hasattr(g, 'start_time'):
duration = time.time() - g.start_time
app.logger.info(
f'{request.method} {request.path} - '
f'{response.status_code} - {duration:.3f}s'
)
return response
Database Backup Script
#!/bin/bash
# backup.sh
set -e
# Configuration
BACKUP_DIR="/var/backups/db"
DATABASE_URL="$DATABASE_URL"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/backup_$DATE.sql"
RETENTION_DAYS=7
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Create backup
echo "Creating database backup..."
pg_dump "$DATABASE_URL" > "$BACKUP_FILE"
# Compress backup
gzip "$BACKUP_FILE"
# Remove old backups
echo "Cleaning up old backups..."
find "$BACKUP_DIR" -name "backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "Backup completed: $BACKUP_FILE.gz"
Deployment Scripts
#!/bin/bash
# deploy.sh
set -e
echo "Starting deployment..."
# Pull latest code
git pull origin main
# Build new Docker image
docker-compose build web
# Run database migrations
docker-compose run --rm web flask db upgrade
# Update services
docker-compose up -d
# Health check
echo "Waiting for application to be ready..."
sleep 10
if curl -f http://localhost:8000/health; then
echo "Deployment successful!"
else
echo "Deployment failed - health check failed"
exit 1
fi
Monitoring with Prometheus
# app/metrics.py
from prometheus_flask_exporter import PrometheusMetrics
from flask import request
import time
def setup_metrics(app):
"""Setup Prometheus metrics."""
metrics = PrometheusMetrics(app)
# Custom metrics
metrics.info('app_info', 'Application info', version='1.0.0')
# Database connection pool metrics
@metrics.gauge('db_pool_size', 'Database connection pool size')
def db_pool_size():
from app.extensions import db
return db.engine.pool.size()
return metrics