This tutorial will show how to properly dockerize a web application written in Flask. I will use Docker-Compose to create two containers. The first one will run the Flask application using the WSGI server gunicorn. The second container will run a nginx reverse proxy. It will serve all static files much faster than the WSGI server could do it. This tutorial assumes you already have installed Docker and Docker-Compose. If not have a look at the installation instructions on Docker and Docker-Compose. Also you should have a running version of Python 3 on your system.

The Flask application

Flask is a Microframework for web applications. In short it allows you to provide dynamic web content based on server-side content e.g. from a database. Flask is amoung the most-known Python web frameworks along with Django and Pyramid.

Our small Flask app will only serve a single page and the logic is pretty straight forward. Please create a new directory with the following structure:

flaskapp
    |- templates
    |- static
    app.py

The file app.py contains our application logic.

app.py:

from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

This code creates a new Flask object and adds an index route ("/") that will serve the template file "index.html". In Flask the templates live in the directory "templates".

templates/index.html:

<!DOCTYPE html>
<html>
<head>
  <title>Flask App</title>
</head>
<body>
  <p>Hello World.</p>
  <img src="{{ url_for('static', filename='logo.png') }}" style="max-width: 240px;">
</body>
</html>

The template contains a small "Hello World" message and an image that is located in the "static" directory. Now, to start the Flask webserver we need to run the following command in our shell:

$ FLASK_APP=app.py flask run

This will serve our web application on port 5000. Use your webbrowser and go to http://localhost:5000 to display the web page.

The Docker container

To create a Docker container we need to add a Dockerfile with the following content.

Dockerfile:

FROM python:3.7.1-stretch
WORKDIR /flask
RUN pip3 install flask gunicorn
COPY . /flask
EXPOSE 5000
CMD gunicorn app:app -b0.0.0.0:5000

This Dockerfile uses the docker image of Python 3.7.1 on Linux stretch. It will install "Flask" and the production WSGI server Gunicorn which is much faster than Flasks build-in webserver.

Now we can build and run this docker image:

$ docker build -t flask_docker .
$ docker run -it --rm -p5000:5000 flask_docker

The first command will build an image named "flask_docker". The second command will create a container from this image. The -it flag enables us to interact with the container from our shell. We need this for being able to stop it via ctrl-C. The flag --rm tells Docker to remove the container once it is stopped. The last argument -p5000:5000 tells Docker to bridge the port 5000. So we are able to access the web-app from our host via http://localhost:5000.

Docker Compose

This does a pretty good job. However there is one drawback. We use our WSGI server to serve dynamic content like the "index" route but also static files like our nice Python logo. It would be better to use a reverse-proxy like nginx to serve our static files. This way we can relieve our WSGI server and it can concentrate on serving the dynamic content. So what we will do is create another container with nginx that will serve our static content and forward all other requests to our "flask_docker" container. For defining and running multi-container applications Docker has a great tool called Docker-Compose.

To define our composition we need to create a new file called "docker-compose.yml".

docker-compose.yml:

version: '3'

services:
  flask:
    build: .
    image: flask_docker 
    volumes:
      - data:/flask
    expose:
      - "5000"
    networks:
      - web

  nginx:
    image: nginx:alpine
    volumes:
      - data: /flask:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
    networks:
      - web

networks:
  web:

volumes:
  data

This defines two services or containers. The first one is our Flask app and will be built from our Dockerfile that we have created earlier. The second service is the nginx reverse proxy. We use the nginx:alpine image from Docker Hub. We share a volume called "data" between the two containers. This will contain the complete web application and it will be mounted to the /flask directory of each container. Also we mount the configuration file nginx.conf that contains the settings for the nginx service. Both containers will be connected via a Docker network called "web". For more info on Docker networks have a look in the Docker documentation. The configuration file for the nginx server is listed below.

nginx.conf:

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;

    upstream docker-flask {
        server flask:5000;
    }

    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    server {
        listen 80;

        location / {
            proxy_pass http://docker-flask;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded_Proto $scheme;
        }

        location ^~ /static/ {
            root /flask;
        }
    }
}

This is basically a default configuration file with some slight modifications. The first one ist the upstream section:

upstream docker-flask {
    server flask: 5000;
}

This tells nginx that there is an upstream server called docker-flask that serves on port 5000. We tell nginx to pass all incoming requests to the upstream server with the following line:

proxy_pass http://docker-flask;

Only the static content should be served by nginx so we need to add the following line at the end:

location ^~ /static/ {
    root /flask;
}

To run our container composition we simply need to call:

$ docker-compose up
Creating network "flask_docker_web" with the default driver
Creating flask_docker_flask_1 ... done
Creating flask_docker_nginx_1 ... done
Attaching to flask_docker_flask_1, flask_docker_nginx_1
flask_1  | [2018-12-19 18:13:41 +0000] [6] [INFO] Starting gunicorn 19.9.0
flask_1  | [2018-12-19 18:13:41 +0000] [6] [INFO] Listening at: http://0.0.0.0:5000 (6)
flask_1  | [2018-12-19 18:13:41 +0000] [6] [INFO] Using worker: sync
flask_1  | [2018-12-19 18:13:41 +0000] [9] [INFO] Booting worker with pid: 9
nginx_1  | 172.20.0.1 - - [19/Dec/2018:18:13:48 +0000] "GET / HTTP/1.1" 200 206 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:64.0) Gecko/20100101 Firefox/64.0" "-"
nginx_1  | 172.20.0.1 - - [19/Dec/2018:18:13:48 +0000] "GET /static/logo.png HTTP/1.1" 200 52166 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:64.0) Gecko/20100101 Firefox/64.0" "-"

On the last line you can see that the GET request to /static/logo.png gets handled by nginx. To stop the containers just press ctrl-c. To run the containers in the background use the command $ docker-compose up -d.

I hope you enjoyed this small tutorial. Additional information can be found on the following links:

You can find the whole project on Github, feel free to fork: https://github.com/stlehmann/flask_docker