From Zero to Hero: A Collection of Flask Production Recipes

As a machine learning engineer specializing in computer vision, I frequently utilize Flask to construct APIs and web applications. In this guide, I’ll outline practical advice and techniques for crafting a production-ready Flask application.

Our exploration encompasses these key areas:

  1. Configuration Management: Production applications typically progress through development, testing, and deployment stages, each necessitating distinct settings for database connections, API keys, and URLs.
  2. Self-Hosting with Gunicorn: While Flask includes a development server, production environments demand robust solutions like Gunicorn, a Python WSGI HTTP server, for handling client requests.
  3. Static File Serving and Proxying with Nginx: Gunicorn, as an application server, benefits from Nginx acting as a reverse proxy, efficiently serving static content and enabling load balancing for scalability.
  4. Dockerized Deployment: Containerization simplifies deployment and management. Our application will be encapsulated within Docker containers for isolation and portability.
  5. PostgreSQL Database Integration: Alembic will manage our database schema and migrations, while SQLAlchemy facilitates object-relational mapping.
  6. Celery Task Queue: Asynchronous task handling is crucial for offloading resource-intensive operations like email sending or image processing to dedicated worker processes, preventing web server bottlenecks.

Constructing the Flask Application

Let’s initiate our application’s structure and codebase. Note that this demonstration prioritizes clarity over intricate Flask application design.

Start by establishing the directory structure and a Git repository.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
mkdir flask-deploy
cd flask-deploy
# init GIT repo
git init
# create folder structure
mkdir static tasks models config 
# install required packages with pipenv, this will create a Pipfile
pipenv install flask flask-restful flask-sqlalchemy flask-migrate celery
# create test static asset
echo "Hello World!" > static/hello-world.txt

Now, let’s populate it with code.

config/__init__.py

This module houses our lightweight configuration management system. It tailors the application’s behavior based on the APP_ENV environment variable, permitting overrides through specific environment variables.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import os
import sys
import config.settings

# create settings object corresponding to specified env
APP_ENV = os.environ.get('APP_ENV', 'Dev')
_current = getattr(sys.modules['config.settings'], '{0}Config'.format(APP_ENV))()

# copy attributes to the module for convenience
for atr in [f for f in dir(_current) if not '__' in f]:
   # environment can override anything
   val = os.environ.get(atr, getattr(_current, atr))
   setattr(sys.modules[__name__], atr, val)


def as_dict():
   res = {}
   for atr in [f for f in dir(config) if not '__' in f]:
       val = getattr(config, atr)
       res[atr] = val
   return res

config/settings.py

Here, configuration classes align with different APP_ENV values. At runtime, __init__.py selects the appropriate class and allows environment variables to supersede defaults. This configuration object will be instrumental in initializing Flask and Celery.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BaseConfig():
   API_PREFIX = '/api'
   TESTING = False
   DEBUG = False


class DevConfig(BaseConfig):
   FLASK_ENV = 'development'
   DEBUG = True
   SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy'
   CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//'
   CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@broker-rabbitmq//'


class ProductionConfig(BaseConfig):
   FLASK_ENV = 'production'
   SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy'
   CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//'
   CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@broker-rabbitmq//'


class TestConfig(BaseConfig):
   FLASK_ENV = 'development'
   TESTING = True
   DEBUG = True
   # make celery execute tasks synchronously in the same process
   CELERY_ALWAYS_EAGER = True

tasks/__init__.py

This segment handles Celery initialization, drawing settings from the config package. This structure accommodates potential Celery-specific configurations, such as task scheduling and worker timeouts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from celery import Celery
import config


def make_celery():
   celery = Celery(__name__, broker=config.CELERY_BROKER)
   celery.conf.update(config.as_dict())
   return celery


celery = make_celery()

tasks/celery_worker.py

This module is dedicated to launching a Celery worker within its Docker container. It initializes the Flask application context for environment consistency, although this can be omitted if not required.

1
2
3
4
5
6
from app import create_app

app = create_app()
app.app_context().push()

from tasks import celery

api/__init__.py

Our REST API, built with Flask-Restful, is defined here. For demonstration, we’ll implement two endpoints:

  • /process_data: Initiates a simulated long-running Celery task, returning the task ID.
  • /tasks/<task_id>: Retrieves the status of a task using its ID.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import time
from flask import jsonify
from flask_restful import Api, Resource
from tasks import celery
import config

api = Api(prefix=config.API_PREFIX)


class TaskStatusAPI(Resource):
   def get(self, task_id):
       task = celery.AsyncResult(task_id)
       return jsonify(task.result)


class DataProcessingAPI(Resource):
   def post(self):
       task = process_data.delay()
       return {'task_id': task.id}, 200


@celery.task()
def process_data():
   time.sleep(60)


# data processing endpoint
api.add_resource(DataProcessingAPI, '/process_data')

# task status endpoint
api.add_resource(TaskStatusAPI, '/tasks/<string:task_id>')

models/__init__.py

We’ll define a SQLAlchemy model for a User object and initialize the database engine. While not actively used in our demo, this ensures proper database migration and SQLAlchemy-Flask integration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import uuid

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()


class User(db.Model):
   id = db.Column(db.String(), primary_key=True, default=lambda: str(uuid.uuid4()))
   username = db.Column(db.String())
   email = db.Column(db.String(), unique=True)

Observe how UUIDs are automatically generated as primary keys.

app.py

This file contains our main Flask application logic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from flask import Flask

logging.basicConfig(level=logging.DEBUG,
                   format='[%(asctime)s]: {} %(levelname)s %(message)s'.format(os.getpid()),
                   datefmt='%Y-%m-%d %H:%M:%S',
                   handlers=[logging.StreamHandler()])

logger = logging.getLogger()


def create_app():
   logger.info(f'Starting app in {config.APP_ENV} environment')
   app = Flask(__name__)
   app.config.from_object('config')
   api.init_app(app)
   # initialize SQLAlchemy
   db.init_app(app)

   # define hello world page

   @app.route('/')
   def hello_world():
       return 'Hello, World!'
   return app


if __name__ == "__main__":
   app = create_app()
   app.run(host='0.0.0.0', debug=True)</td>
  </tr>
  <tr>
    <td>

Key points:

  • Structured logging with timestamps, log levels, and process IDs.
  • Flask app creation, including API initialization and a basic “Hello, world!” route.
  • An entry point for development-time execution.

wsgi.py

This module facilitates running Flask with Gunicorn:

1
2
from app import create_app
app = create_app()

With the application code ready, we’ll proceed to Docker configuration.

Docker Containerization

Our application will be distributed across several Docker containers:

  1. Application Container: Serves templates and exposes API endpoints. While ideal to separate these functions in production, our demo combines them. This container runs the Gunicorn web server, communicating with Flask via WSGI.
  2. Celery Worker Container: Executes asynchronous tasks. Essentially the application container with a command to start Celery instead of Gunicorn.
  3. Celery Beat Container: Similar to the worker container, but dedicated to scheduled tasks like email confirmations.
  4. RabbitMQ Container: Celery requires a message broker like RabbitMQ, Redis, or Kafka for communication and task result storage.
  5. PostgreSQL Container: Houses our database.

Docker Compose simplifies the management of these containers. First, we’ll define a Dockerfile for our application container in the project root.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
FROM python:3.7.2

RUN pip install pipenv

ADD . /flask-deploy

WORKDIR /flask-deploy

RUN pipenv install --system --skip-lock

RUN pip install gunicorn[gevent]

EXPOSE 5000

CMD gunicorn --worker-class gevent --workers 8 --bind 0.0.0.0:5000 wsgi:app --max-requests 10000 --timeout 5 --keep-alive 5 --log-level info

This Dockerfile instructs Docker to:

  • Install Python dependencies.
  • Add the application code.
  • Expose port 5000.
  • Set the default command to launch Gunicorn, utilizing the gevent worker class for efficient asynchronous handling with Gevent. The --workers parameter, ideally matching the server’s core count, dictates the number of worker processes.

With our application container defined, let’s create docker-compose.yml to orchestrate all containers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
version: '3'
services:
 broker-rabbitmq:
   image: "rabbitmq:3.7.14-management"
   environment:
     - RABBITMQ_DEFAULT_USER=rabbit_user
     - RABBITMQ_DEFAULT_PASS=rabbit_password
 db-postgres:
   image: "postgres:11.2"
   environment:
     - POSTGRES_USER=db_user
     - POSTGRES_PASSWORD=db_password
 migration:
   build: .
   environment:
     - APP_ENV=${APP_ENV}
   command: flask db upgrade
   depends_on:
     - db-postgres
 api:
   build: .
   ports:
    - "5000:5000"
   environment:
     - APP_ENV=${APP_ENV}
   depends_on:
     - broker-rabbitmq
     - db-postgres
     - migration
 api-worker:
   build: .
   command: celery worker --workdir=. -A tasks.celery --loglevel=info
   environment:
     - APP_ENV=${APP_ENV}
   depends_on:
     - broker-rabbitmq
     - db-postgres
     - migration
 api-beat:
   build: .
   command: celery beat -A tasks.celery --loglevel=info
   environment:
     - APP_ENV=${APP_ENV}
   depends_on:
     - broker-rabbitmq
     - db-postgres
     - migration

We’ve defined these services:

  • broker-rabbitmq: A RabbitMQ container with environment-based credentials.
  • db-postgres: A PostgreSQL container with its credentials.
  • migration: A temporary container to apply database migrations using Flask-Migrate. API containers depend on its completion.
  • api: Our main application container.
  • api-worker and api-beat: Containers running Celery workers for on-demand and scheduled tasks.

The APP_ENV variable is passed to each app container during startup.

Let’s push our code to GitHub for deployment convenience.

1
2
3
4
git add *
git commit -a -m 'Initial commit'
git remote add origin git@github.com:your-name/flask-deploy.git
git push -u origin master

Server Setup

Assuming an AWS instance (adjust instructions for other Linux distributions) with an external IP, configured DNS, and SSL certificates, we’ll finalize our server configuration.

Security Best Practice: Configure your firewall (e.g., using iptables) to allow incoming traffic on ports 80 (HTTP), 443 (HTTPS), and 22 (SSH) only. Ensure these restrictions apply to both IPv4 and IPv6.

Dependency Installation

We’ll install Nginx, Docker, and Git:

1
sudo yum install -y docker docker-compose nginx git

Nginx Configuration

While the default nginx.conf often suffices, we’ll create a dedicated configuration file within the conf.d directory, which Nginx automatically includes.

1
2
cd /etc/nginx/conf.d
sudo vim flask-deploy.conf

This Flask-specific Nginx configuration features:

  1. SSL Enabled: Assumes valid SSL certificates, obtainable for free from providers like Let’s Encrypt.
  2. Canonical Domain Redirection: Redirects www.your-site.com to your-site.com.
  3. HTTPS Redirection: Enforces HTTPS connections.
  4. Reverse Proxy: Forwards requests to the application on port 5000.
  5. Static File Serving: Leverages Nginx for efficient static content delivery.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
server {
    listen   80;
    listen   443;
    server_name  www.your-site.com;
    # check your certificate path!
    ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers HIGH:!aNULL:!MD5;
    # redirect to non-www domain
    return   301 https://your-site.com$request_uri;
}

# HTTP to HTTPS redirection
server {
        listen 80;
        server_name your-site.com;
        return 301 https://your-site.com$request_uri;
}

server {
        listen 443 ssl;
        # check your certificate path!
        ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt;
        ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:!aNULL:!MD5;
        # affects the size of files user can upload with HTTP POST
        client_max_body_size 10M;
        server_name your-site.com;
        location / {
                include  /etc/nginx/mime.types;
                root /home/ec2-user/flask-deploy/static;
                # if static file not found - pass request to Flask
                try_files $uri @flask;
        }
  location @flask {
                add_header 'Access-Control-Allow-Origin' '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';

                proxy_read_timeout 10;
                proxy_send_timeout 10;
                send_timeout 60;
                resolver_timeout 120;
                client_body_timeout 120;
                
                # set headers to pass request info to Flask
                proxy_set_header   Host $http_host;
                proxy_set_header   X-Forwarded-Proto $scheme;
                proxy_set_header   X-Forwarded-For $remote_addr;
                proxy_redirect     off;
    
                proxy_pass http://127.0.0.1:5000$uri;
        }
}

After saving, reload Nginx with sudo nginx -s reload and check for errors.

GitHub Deployment Key

Creating a dedicated GitHub deployment key enhances security by avoiding the use of personal credentials and allowing for fine-grained access control.

Generate an SSH key on the server:

1
2
cd ~/.ssh
ssh-keygen -b 2048 -t rsa -f id_rsa.pub -q -N "" -C "deploy"

Add the contents of ~/.ssh/id_rsa.pub as a add your public key in your GitHub account settings.

Deployment

Finally, let’s fetch the code and initiate our containers.

1
2
3
4
cd ~
git clone https://github.com/your-name/flask-deploy.git
git checkout master
APP_ENV=Production docker-compose up -d

Omitting -d (detached mode) initially can be helpful to monitor container output for errors. Use docker logs to inspect individual container logs later. Verify container status with docker ps.

image_alt_text

Success! All five containers are operational. Docker Compose automatically assigns names based on our docker-compose.yml configuration. Let’s test our setup from an external machine.

1
2
3
4
5
6
# test HTTP protocol, you should get a 301 response
curl your-site.com
# HTTPS request should return our Hello World message
curl https://your-site.com
# and nginx should correctly send test static file:
curl https://your-site.com/hello-world.txt

Congratulations! We’ve successfully deployed a production-ready Flask application on AWS. The complete codebase is available on a GitHub repository.

Looking Ahead

While we’ve covered core aspects, a comprehensive production setup involves further considerations:

  • Continuous Integration and Continuous Deployment (CI/CD)
  • Automated Testing
  • Centralized Logging
  • API Monitoring and Error Tracking
  • Horizontal Scaling
  • Secure Credential Management

These topics are explored in resources like “Python Logging: An In-Depth Tutorial” and “How to Build an Effective Initial Deployment Pipeline” available on this blog. I encourage you to explore these areas to enhance your application’s robustness and maintainability.

Thank you for embarking on this journey towards building production-ready Flask applications!

Licensed under CC BY-NC-SA 4.0