Skip to main content

Running Flask in production: a comprehensive guide

A comprehensive guide to deploying Flask applications in production using Docker, with a focus on performance, reliability, and scalability.

Introduction

Flask's built-in development server is perfect for local testing but notably unsuitable for production environments. This development server—while convenient—lacks critical production features, resulting in performance bottlenecks, request timeouts, and security vulnerabilities when faced with real-world traffic. A proper production deployment requires a purposefully designed application server, a robust process manager, and often a front-facing web server to handle client connections efficiently.

This guide explores production-ready deployment options for Flask applications, with a primary focus on Docker-based solutions. As an advanced user, you'll learn how to configure, deploy, and maintain Flask applications that can reliably serve production traffic.

Flask production server fundamentals

Why the development server falls short

Flask's development server was designed to simplify the development process, but it has several limitations that make it unsuitable for production:

  • Single-threaded by default: Unable to handle concurrent requests efficiently
  • No built-in process management: No automatic restarts after crashes
  • Limited security features: Not hardened against various attack vectors
  • Poor performance under load: Significant degradation with increased traffic
  • No built-in TLS/SSL support: Requires additional configuration for HTTPS

Essential components of a production setup

A robust Flask production environment typically includes:

  1. WSGI server: Translates web requests to Python calls (uWSGI, Gunicorn)
  2. Web server: Handles client connections, static files, SSL termination (Nginx, Apache)
  3. Process manager: Ensures application availability and handles crashes
  4. Container or virtualisation: Isolates the application and its dependencies
  5. Monitoring and logging: Provides visibility into application behaviour

Docker-based Flask deployment

Docker has become the preferred method for deploying Flask applications due to its consistency, isolation, and orchestration capabilities. Let's explore the main approaches.

Option 1: Flask with Gunicorn in a single container

This approach is straightforward and works well for smaller applications:

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Run gunicorn with 4 worker processes
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "wsgi:app"]

The corresponding wsgi.py file:

from my_app import create_app

app = create_app()

if __name__ == "__main__":
    app.run()

This setup works but lacks the performance benefits of a dedicated web server for static files and connection handling.

Option 2: Flask with Gunicorn and Nginx (multi-container)

This more robust approach uses Docker Compose to manage multiple containers:

# docker-compose.yml
version: '3.8'

services:
  flask:
    build: ./app
    container_name: flask_app
    restart: always
    environment:
      - FLASK_ENV=production
    networks:
      - app_network

  nginx:
    build: ./nginx
    container_name: nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/ssl:/etc/nginx/ssl
      - ./app/static:/static
    depends_on:
      - flask
    networks:
      - app_network

networks:
  app_network:

Flask application Dockerfile:

# app/Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "wsgi:app"]

Nginx Dockerfile:

# nginx/Dockerfile
FROM nginx:1.25-alpine

COPY nginx.conf /etc/nginx/conf.d/default.conf

Nginx configuration:

# nginx/nginx.conf
server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://flask:5000;
        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;
    }

    location /static {
        alias /static;
        expires 30d;
    }
}

Option 3: Flask with uWSGI and Nginx

While Gunicorn is popular for its simplicity, uWSGI offers more advanced features and potentially better performance:

# docker-compose.yml
version: '3.8'

services:
  app:
    build: ./app
    restart: always
    networks:
      - app_network
    volumes:
      - ./app:/app

  nginx:
    build: ./nginx
    ports:
      - "80:80"
    networks:
      - app_network
    depends_on:
      - app

networks:
  app_network:

Flask with uWSGI Dockerfile:

# app/Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt uwsgi

COPY . .

CMD ["uwsgi", "--ini", "uwsgi.ini"]

uWSGI configuration:

# app/uwsgi.ini
[uwsgi]
module = wsgi:app
uid = www-data
gid = www-data
master = true
processes = 5

socket = /tmp/uwsgi.sock
chmod-socket = 664
vacuum = true

die-on-term = true

Nginx configuration for uWSGI:

# nginx/nginx.conf
server {
    listen 80;
    server_name example.com;

    location / {
        include uwsgi_params;
        uwsgi_pass unix:///tmp/uwsgi.sock;
    }

    location /static {
        alias /app/static;
    }
}

Performance optimisation

Worker configuration

The number of workers is a critical configuration parameter that affects performance. A common formula is:

workers = (2 × CPU cores) + 1

For Gunicorn, you can set this with:

gunicorn --workers=5 --threads=2 wsgi:app

For uWSGI:

[uwsgi]
processes = 5
threads = 2

Connection handling

Optimize how your server handles connections:

# nginx/nginx.conf (additional optimizations)
http {
    # Connection timeouts
    keepalive_timeout 65;
    client_body_timeout 10;
    client_header_timeout 10;
    send_timeout 10;

    # Buffer sizes
    client_body_buffer_size 128k;
    client_header_buffer_size 1k;
    client_max_body_size 10m;
    large_client_header_buffers 4 4k;

    # Compression
    gzip on;
    gzip_min_length 1000;
    gzip_types text/plain text/css application/json application/javascript;
}

Scaling with Docker Swarm or Kubernetes

For larger applications, consider orchestration:

# docker-stack.yml for Docker Swarm
version: '3.8'

services:
  flask:
    image: yourusername/flask-app:latest
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
    networks:
      - app_network

  nginx:
    image: yourusername/nginx:latest
    ports:
      - "80:80"
    networks:
      - app_network
    deploy:
      replicas: 2
    depends_on:
      - flask

networks:
  app_network:
    driver: overlay

Monitoring and logging

Prometheus and Grafana setup

# Add to docker-compose.yml
services:
  # ... other services

  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus:/etc/prometheus
    ports:
      - "9090:9090"
    networks:
      - app_network

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    networks:
      - app_network
    depends_on:
      - prometheus

Flask application instrumentation:

# app.py
from flask import Flask
from prometheus_flask_exporter import PrometheusMetrics

app = Flask(__name__)
metrics = PrometheusMetrics(app)

# Static information as metric
metrics.info('app_info', 'Application info', version='1.0.3')

@app.route('/')
def main():
    return "Hello World!"

if __name__ == '__main__':
    app.run(host='0.0.0.0')

Centralised logging with ELK stack

# Add to docker-compose.yml
services:
  # ... other services

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.16.2
    environment:
      - discovery.type=single-node
    volumes:
      - elasticsearch-data:/usr/share/elasticsearch/data
    networks:
      - app_network

  logstash:
    image: docker.elastic.co/logstash/logstash:7.16.2
    volumes:
      - ./logstash/pipeline:/usr/share/logstash/pipeline
    networks:
      - app_network
    depends_on:
      - elasticsearch

  kibana:
    image: docker.elastic.co/kibana/kibana:7.16.2
    ports:
      - "5601:5601"
    networks:
      - app_network
    depends_on:
      - elasticsearch

volumes:
  elasticsearch-data:

Configure Flask to use the ELK stack:

# logging_config.py
import logging
from logging.handlers import SocketHandler

def configure_logging(app):
    logstash_handler = SocketHandler('logstash', 5000)
    app.logger.addHandler(logstash_handler)
    app.logger.setLevel(logging.INFO)

Security considerations

Environment variables for sensitive data

# config.py
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    # other configuration variables
# docker-compose.yml (excerpt)
services:
  flask:
    # ... other configuration
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - DATABASE_URL=${DATABASE_URL}

HTTPS configuration with Let's Encrypt

# nginx/nginx.conf
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    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_prefer_server_ciphers on;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
    ssl_session_cache shared:SSL:10m;

    location / {
        proxy_pass http://flask:5000;
        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;
    }
}

Best practices and common pitfalls

Best practices

  • Use application factories: Makes testing and configuration easier
  • Environment-specific configuration: Different settings for development, testing, and production
  • Health checks: Monitor application health and restart when necessary
  • Graceful shutdown: Handle termination signals properly
  • Connection pooling: Optimize database connections

Common pitfalls

  • Overloading worker processes: Leads to degraded performance
  • Memory leaks: Gradually consume all available memory
  • Inadequate error handling: Causes application crashes
  • Misconfigured proxy headers: Security vulnerabilities and broken functionality
  • Improper static file handling: Performance issues

Conclusion

Properly deploying Flask in a production environment requires careful consideration of various components. Docker provides an excellent platform for creating consistent, isolated, and scalable deployments. By following the best practices outlined in this guide, you can ensure your Flask application is robust, performant, and maintainable in production.

For your next steps, consider exploring more advanced topics such as blue-green deployments, A/B testing infrastructure, and automated scaling based on traffic patterns. These approaches can further enhance your Flask application's reliability and performance in production environments.