Scaling a Python Flask App with NGINX using Multiple Containers with Docker Compose

July 15, 2019

This article will go over scaling a Python Flask application utilizing a multi-container docker architecture. Leveraging Docker Compose we will create a NGINX Docker container that will act as a load balancer with two Python Flask application containers it will direct traffic to. The Python Flask application will serve a web page via a GET request and will be running Gunicorn.

Assumptions:

  • You have Docker installed
  • These instructions are were done on a Mac
  • You are leveraging multiple containers on one host (docker engine), if not read the bottom of this post about “docker stack deploy”.

Quick Links:

GitHub repo with files referenced in this blog ubuntu-flask-gunicorn-nginx-docker-compose

Docker Hub repo with some of the images used in the blog here.

File Structure:


ubuntu-flask-gunicorn-nginx-docker-compose/
|
docker-compose.yml
|
app/
| –> Dockerfile
| –> src
| –> app01.py
| –> gunicorn_config.py
| –> wsgi.py
| –> requirements.txt
|
nginx/
–> Dockerfile
–> nginx.conf

Step 0
Create a directory for your project.

mkdir ubuntu-flask-gunicorn-nginx-docker-compose

cd ubuntu-flask-gunicorn-nginx-docker-compose

mkdir -p app/src

mkdir nginx


Step 1
Create a file requirements.txt for your Python dependencies, such as Flask or Gunicorn. For more information about PIP requirements.txt read here.

Add the following to the requirements.txt

Click==7.0
Flask==1.0.3
gunicorn==19.9.0
itsdangerous==1.1.0
Jinja2==2.10.1
MarkupSafe==1.1.1
Werkzeug==0.15.4

Step 2
Create a file app01.py which will be a simple Flask web app. Flask is a microframework for Python based on Werkzeug and Jinja2. For more information about Flask read here.

Add the following to app01.py

from flask import Flask
hello = Flask(__name__)

@hello.route("/")
def greeting():
    return "

Hello World!

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

Step 3
Create a file wsgi.py which will be the gateway from the webserver to your app. For more information about WSGI read here.

Add the following to wsgi.py

from app01 import hello

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

Step 4
Create a file gunicorn_config.py which will be the configurations Gunicorn will utilize. Gunicorn is a Python WSGI HTTP Server for UNIX. For more information about Gunicorn read here.

Add the following to gunicorn_config.py
*Note we set workers to 2 because if you only have one worker, and it’s handling a slow query, the heartbeat query will timeout which could remove it from a load balancer. Also container schedulers expect logs to come out on stdout and stderr so we have it set in the config as such. We also leverage /dev/shm vs default /tmp to avoid timeouts accessing ram vs disk.

pidfile = 'app01.pid'
worker_tmp_dir = '/dev/shm'
worker_class = 'gthread'
workers = 2
worker_connections = 1000
timeout = 30
keepalive = 2
threads = 4
proc_name = 'app01'
bind = '0.0.0.0:8080'
backlog = 2048
accesslog = '-'
errorlog = '-'
user = 'ubuntu'
group = 'ubuntu'

Step 5
Create a Dockerfile which contains the commands to build your Python Flask App Docker image. For more information on Dockerfile read here.

Add the following to Dockerfile
*Note we pull a base ubuntu 18.04 image with python 3.7.3 from my Docker Hub repo you could point this to the official Ubuntu repo and add a Python image or install line. All the files are copied into the /home/ubuntu container directory and referenced from there.

FROM nethacker/ubuntu-18-04-python-3:python-3.7.3
COPY src/requirements.txt /root/
RUN pip install -r /root/requirements.txt && useradd -m ubuntu
ENV HOME=/home/ubuntu
USER ubuntu
COPY src/app01.py src/wsgi.py src/gunicorn_config.py /home/ubuntu/
WORKDIR /home/ubuntu/
EXPOSE 8080
CMD ["gunicorn", "-c", "gunicorn_config.py", "wsgi:hello"]

Step 6
Create a NGINX configuration file to make a reverse proxy load balancer to your Python Flask application

cd ../nginx/

vim nginx.conf

Add the following to your nginx.conf file for NGINX to act as a reverse proxy load balancer.

events { worker_connections 1024; }

http {

 proxy_headers_hash_max_size 1024;
 proxy_headers_hash_bucket_size 64;

 upstream localhost {
    # References to our app containers, via docker compose
    server app01:8080;
    server app02:8080;
 }
 server {
    listen 80;
    server_name localhost;
    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP      $remote_addr;
        proxy_redirect off;
        proxy_buffers 8 24k;
        proxy_buffer_size 4k;
        proxy_pass http://localhost;
        proxy_set_header Host $host;
    }
  }
}

Step 7
Create a file Dockerfile which contains the commands to build your NGINX Docker image.

Add the following to Dockerfile

FROM nethacker/ubuntu-18-04-nginx:1.17.1
RUN apt-get update && apt-get install -y \
    build-essential \
    curl \
  && rm -rf /var/lib/apt/lists/*
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Step 8
Create your Docker Compose YAML file outlining one NGINX container with two backend Python Flask application containers to which to direct traffic too. Also specify the NGINX port as well increasing shm size for Gunicorn to use.

cd ../

vim docker-compose.yml

Add the following to docker-compose.yml file.

version: '3.7'
services:
    app01:
        shm_size: '1000000000'
        build:
            context: ./app
        tty: true
        volumes:
            - './app/src:/home/ubuntu'
    app02:
        shm_size: '1000000000'
        build:
            context: ./app
        tty: true
        volumes:
            - './app/src:/home/ubuntu'
    nginx:
        build: ./nginx
        tty: true
        links:
            - app01
            - app02
        ports:
            - '80:80'

Step 9
Build and Start your Docker images using Docker Compose, you will end up with one NGINX container with two backend Python Flask application containers. The NGINX container will listen on port 80 and forward traffic to the backend apps on 8080.

docker-compose up --build --detach

Step 10
Test your Docker container

If you didn’t modify the example you should be able to go to localhost port 80 in a browser and get a red “Hello World” message.

You can also do the following to access your containers with a bash shell to poke around.

Find the running Docker container id you wish to examine.

Get and interactive shell on the container.

docker exec -it {id here} /bin/bash

Hopefully this basic overview of Docker Compose and multi-containers on a single host/docker engine helps you scale your application.

On a side note,

command appears to be transitioning to:

with the difference “docker-compose” can do builds in the docker-compose.yml and 2.0/3.0 spec with caveats, but “docker stack deploy” cannot do build commands in the docker-compose.yml and needs prebuilt images as well as 3.0 spec making docker compose nicer for development purposes but “docker stack deploy” a more production oriented method with multiple containers over multiple hosts leveraging swarm. In both cases docker-compose.yml is used and Docker will ignore commands not support by the respective compose vs stack call (silently). I will do a post on Docker Swarm next.

Comments are closed.