How I Dockerize My Flask App With Pypy and Supervisord

December 19, 2018
Python Flask Engineering

There are bunch of ways how to deploy your Flask application, it can be directly with Flask’s built in http server or use UWSGI as gateway interface, with or without Nginx as your HTTP frontend. People also either run Flask application directly on their machine or as a container with docker.

In this post, I will share the way I deploy my Flask application. Lately, I prefer to use PyPy rather than using pure Python to achieve more speed processing time (thanks to its JIT Compiler), and use Gevent to handle more concurrency.

*FYI, In my current office, we rely much on Docker as the application container to ease developers with their development environment.

Here is my Flask application structure. This is the basic directory and file structure as an application boilerplate.

Kinaras-MacBook-Pro:flask-pypy-docker kinara$ tree .
.
├── Dockerfile
├── README.md
├── app
│   └── __init__.py
├── config.py
├── docker-compose.yml
├── flask-gunicorn-gevent.sh
├── nginx-conf
│   └── lavender.conf
├── requirements.txt
├── run.py
└── supervisord-conf
    ├── lavender.ini
    └── nginx.ini

Flask Minimum Application

For the application, we can only focus on app/ directory and all the configs will be in config.py

#filename:app/__init__.py
#!/usr/bin/env python 

from datetime import datetime
from flask import Flask, jsonify, url_for

# app 
app = Flask(__name__, instance_relative_config=True)

# load config 
app.config.from_object('config')
app.config.from_pyfile('application.cfg', silent=True)

# add default route 
@app.route('/', methods=['GET'])
def index():
    response = jsonify({
        'msg': 'Lavender',
        'systime': int(datetime.now().strftime('%s'))
    })

    response.status_code = 200
    return response

Okay, then we focus on Dockerfile and docker-compose.yml

# filename: Dockerfile

from pypy:3-6-slim # get pypy image from dockerhub

# install essential package
run apt update 
run apt install -y libpython3-dev build-essential libpcre3 libpcre3-dev 
run apt install -y libssl-dev libffi-dev 
run apt install -y nginx supervisor 

# add this line to make supervisor include *.ini config file from /etc/supervisor/conf.d/
run echo "files = /etc/supervisor/conf.d/.*ini" >> /etc/supervisor/supervisord.conf 

# create user 
ARG user=lavender
ARG group=lavender
ARG uid=1000
ARG gid=1001
RUN adduser ${user}

USER ${user}
RUN mkdir /home/${user}/src
RUN mkdir /home/${user}/log

# copy all application directory
ADD app/ /home/${user}/src/app/
ADD config.py /home/${user}/src/
ADD requirements.txt /home/${user}/src/

# copy gunicorn + gevent script 
ADD flask-gunicorn-gevent.sh /home/${user}/src/

USER root 
# install requirements package
RUN pip install -r /home/${user}/src/requirements.txt

# nginx conf + supervisord conf
ADD nginx-conf/lavender.conf /etc/nginx/conf.d/
ADD supervisord-conf/ /etc/supervisor/conf.d/

# to expose port 8001
EXPOSE 8001

# start supervisord
CMD ["supervisord", "--nodaemon", "-c", "/etc/supervisor/supervisord.conf"]

Also, during development process, and to ease the developers, we also provide docker-compose file with auto reload flask application, so all changes in our application code will also trigger the application to reload. So, there’s no need to docker build and docker run in every change.

#filename: docker-compose.yml
version: '3'
services: 
  web:
    container_name: "lavender_web"
    build: .
    volumes:
      - .:/home/lavender/src
    ports:
      - 8001:8001
    environment: 
      - FLASK_APP=/home/lavender/src/run.py
      - FLASK_DEBUG=1
      - ENV=devel 
    command: flask run --host=0.0.0.0 --port=8001 # this is where the magic of auto reload

We’ve already had the application’s files and the dockerfile, the rest are supervisor files to run gunicorn + gevent and nginx. Yes, we manage nginx + application service with supervisord.

First, here is the bash script to run our Flask application. The script below will create a socket application and will execute gevent as application`s worker.

#filename: flask-gunicorn-gevent.sh
#!/bin/sh
gunicorn --name 'Lavender - Gunicorn' --user lavender --chdir ./ --bind unix:/home/lavender/lavender.sock app:app -k gevent --worker-connections 1001 --workers 4

Below, is our Nginx configuration file as a proxy pass to our application socket

#filename: lavender.conf
server {
        server_name _;
        listen 8001;

        client_max_body_size 4M;

        location / {
                include proxy_params;
                uwsgi_param X-Real-IP $remote_addr;
                uwsgi_param Host $http_host;
                proxy_pass http://unix:/home/lavender/lavender.sock;
        }
}

to manage Nginx and Gunicorn process, we use supervisord.

;flename: lavender.ini
[program:lavender]
command = /home/lavender/src/flask-gunicorn-gevent.sh 
directory=/home/lavender/src/
user=lavender
autostart=true
autorestart=true
stdout_logfile=/home/lavender/log/lavender.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=5
stderr_logfile=/home/lavender/log/lavender.log
stderr_logfile_maxbytes=5MB
stderr_logfile_backups=5
;filename: nginx.ini
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/home/lavender/log/nginx.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=5
stderr_logfile=/home/lavender/log/nginx.log
stderr_logfile_maxbytes=5MB
stderr_logfile_backups=5

To run our application, we can use either docker-compose or Docker build, then followed with docker run image.

To run with docker compose, it is just simple by typing docker-compose up in the root directory or building with docker image

$ docker-compose up --build 
# or
$ docker build -t lavender
$ docker run lavender

to access our application just curl localhost:8001

$ curl localhost:8001
{
  "msg": "Lavender",
  "systime": 1545639346
}

The complete code of this project can be accessed on my Github repository: https://github.com/yudanta/flask-pypy-docker

comments powered by Disqus