A scalable Flask application using Gunicorn on Ubuntu 18.04 in Docker

July 9, 2019

This article will cover creating a scalable Flask application using Gunicorn on Ubuntu 18.04 in Docker. We loosely are defining Flask application to mean serving up a web page via a GET request, however this blog can be used as an example to build a website, Restful API, or any number of things leveraging Docker. Via a Dockerfile we will build a Docker image that you can modify for your own Flask application needs, the example in this blog will be a simple “hello world” web page.

Quick Links:

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

Docker Hub repo for the example app referenced in this blog nethacker/ubuntu-18-04-flask-gunicorn-example

Docker Hub repo for Ubuntu with Flask and Gunicorn with no app nethacker/ubuntu-18-04-flask-gunicorn

Assumptions:

  • You have Docker installed
  • These instructions are were done on a Mac
  • Step 0
    Create a directory for your project.

    mkdir ubuntu-flask-gunicorn-docker
     
    cd ubuntu-flask-gunicorn-docker

    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.

    vim requirements.txt

    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.

    vim app01.py

    Add the following to app01.py

    from flask import Flask
    hello = Flask(__name__)
     
    @hello.route("/")
    def greeting():
        return "<h1 style='color:red'>Hello World!</h1>"
     
    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.

    vim wsgi.py

    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.

    vim gunicorn_config.py

    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 file Dockerfile which contains the commands to build your Docker image. For more information on Dockerfile read here.

    vim Dockerfile

    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 requirements.txt /root/
    RUN pip install -r /root/requirements.txt && useradd -m ubuntu
    ENV HOME=/home/ubuntu
    USER ubuntu
    COPY app01.py wsgi.py gunicorn_config.py /home/ubuntu/
    WORKDIR /home/ubuntu/
    EXPOSE 8080
    CMD ["gunicorn", "-c", "gunicorn_config.py", "wsgi:hello"]

    Step 6
    Build your Docker image
    *Note you can -t tag this with your own naming convention

    docker build -t example/ubuntu-flask-gunicorn-docker:latest .

    Step 7
    Start your Docker container from the Docker image you built. Docker container environments are different then VM’s because of this we set –shm-size to a bigger shared memory size. Gunicorn has a config entry to use shared memory (/dev/shm) vs disk (/tmp) for Gunicorn health checks to avoid timeouts accessing ram vs disk. The setting 80:8080 sets the Docker container to map localhost 80 to container port 8080.

    docker run --shm-size=256m --detach -p 80:8080 example/ubuntu-flask-gunicorn-docker:latest

    Step 8
    Test your Docker container

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

    http://localhost

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

    Find the running Docker container id.

    docker ps

    Get and interactive shell on the container.

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

    Hopefully this basic overview helps you build your own containers!

    Comments are closed.