Gunicorn and Flask
Sep 9, 2020
Web applications seem to be everywhere nowadays. Python is my go to programming language and so I thought I would build a web application using Flask, a lightweight WSGI web application framework. Starting out with Flask is simple enough, but if you want advanced features such as database integration or user logins, then you will either have to build them yourself or rely on the rich Flask extension community.
I recently finished CS50: Introduction to Computer Science class on edX. I highly recommend the class, it changed the way I think about computers, algorithms, and coding. I wrote a web app for my CS50 final project called Recipes. This simple application allows you to store, edit, and view recipes quickly and easily using a web interface.
Containers are my current method of choice for running applications at home, so I decided I wanted a way to deploy Recipes using containers. Additionally I wanted to utilize a production ready HTTP server instead of the default Flask server. I decided on Gunicorn, or Green Unicorn, as my HTTP server of choice. I messed around with uWSGI for a bit, but found its installation and configuration tricky at best. Gunicorn is written in Python and uses a pre-fork model, where a master process creates workers that handle each request. Gunicorn is also easy on system resources and a breeze to deploy.
Assuming python3 is already installed on your system, installation of Gunicorn is as easy as:
python3 -m pip install gunicorn
Gunicorn could be deployed using this one-liner, where the main Flask file in this case is recipes.py:
We will however want to define some additional configuration. Let’s start with the finished recipes app which has the following directory structure:
recipes ├── LICENSE ├── MANIFEST.in ├── README.md ├── recipes │ ├── forms.py │ ├── helpers.py │ ├── __init__.py │ ├── recipes.py │ ├── schema.sql │ ├── static │ │ ├── favicon.ico │ │ └── styles.css │ └── templates │ ├── docs.html │ ├── edit.html │ ├── import.html │ ├── index.html │ ├── layout.html │ ├── login.html │ ├── new.html │ ├── page_not_found.html │ ├── recipe.html │ ├── register.html │ ├── search.html │ └── share.html └── setup.py
In the root of our project directory let’s create a config directory and a new gunicorn.conf.py file:
$ mkdir config
bind = "0.0.0.0:8288" chdir = "/opt/recipes/recipes" worker_tmp_dir = "/dev/shm" workers = 2
Gunicorn configuration files have a simple structure. Here we are defining the bind port, deployment directory, worker temporary directory and worker number. Here’s a great article on configuring Gunicorn for Docker.
Gunicorn uses a health check system on its workers and restarts them if they die. This check process uses a file on the filesystem and Gunicorn recommends this file be stored in memory. The default directory for this check file is in /tmp, and Docker containers do not have /tmp on tmpfs by default. This can sometimes lead to hang of all Gunicorn workers for up to 30 seconds. A quick fix is to tell Gunicorn to store its temporary file in /dev/shm, shared memory, which uses tmpfs.
Now let’s create a simple Python healthcheck script, healthcheck.py:
import requests try: r = requests.get("http://127.0.0.1:8288") if str(r) == "<Response >": print(0) else: print(1) except: print(1)
If the script receives a 200 response from http://127.0.0.1:8288 then it will return 0 (sucessful), otherwise the script will return 1 (unsuccessful).
Next is a an entrypoint Bash script, entrypoint.sh:
#!/bin/bash export SECRET_KEY=$(python3 -c "import os; print(os.urandom(16))") gunicorn -c /config/gunicorn.conf.py recipes:app
A subshell is used to generate a random 16 byte secret key which is assigned to the environment variable SECRET_KEY. Flask will use this secret key to cryptographically sign cookies. The last line is our actual gunicorn command. The -c flag is used to load configuration, here from /config/gunicorn.conf.py.
Note: I had issues when assigning random keys directly in recipes.py. Forms with CSRF protection could not be implemented properly as the secret key changed every time recipes.py was run.
Lastly we’ll create a Dockerfile, bringing all the new components together. I used the python:slim (Debian) image as a base:
FROM python:slim COPY healthcheck.py entrypoint.sh README.md MANIFEST.ini setup.py /opt/recipes/ COPY recipes /opt/recipes/recipes COPY config /config WORKDIR /opt/recipes RUN apt-get update && apt-get install --no-install-recommends -y tini \ && apt-get clean \ && apt-get autoclean \ && apt-get autoremove -y \ && rm -rf \ /tmp/* \ /var/lib/apt/lists/* \ /var/tmp/* RUN python3 setup.py install \ && useradd -r recipes \ && chown -R recipes:recipes /opt/recipes /config VOLUME /config EXPOSE 8288 USER recipes HEALTHCHECK --interval=5m --timeout=3s \ CMD ["python3", "healthcheck.py"] ENTRYPOINT ["/usr/bin/tini", "--", "/opt/recipes/entrypoint.sh"]
Going through the Dockerfile item by item:
- Everything needed by the container is copied over to /opt/recipes. This includes the healthcheck script, entrypoint script, readme file, manifest file, setup file, and inner recipes directory
- Tini is installed to allow for proper SIGTERM termination of Gunicorn
- Recipes is installed as a Python application
- recipes is added as a user
- /config is used as a volume for storing Gunicorn configuration and the SQLite database file
- The healthcheck is implemented and the entrypoint is specified using Tini and our entrypoint script
Recipes can now be deployed with:
docker run -d \ -- name recipes \ -p 8288:8288 \ -v recipe_config:/config \ --restart unless-stopped \ petebuffon/recipes
Using containers to deploy Flask applications with Gunicorn is as easy as that. You could stop there and deploy the application as is or implement a reverse proxy such as Nginx as well. A reverse proxy has the benefit of handling requests and passing them on to Gunicorn, reducing the load on Gunicorn, as well as being able to handle SSL termination.
If your interested in a pre-build version of Recipes, it is also available from Docker Hub.