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