Dockerizing Flask Microservices for Deployment

Creating Python Microservices, Part 3

We’ve created a simple Python microservice using Flask in Part 1 and set up some simple validation in Part 2. In Part 3 we will package up what we’ve built in a Docker container so it can be deployed in Kubernetes.

Code for this can be found on GitHub. Or you can use this template as a starting point.

Because Flask doesn’t handle parallel execution or networking, we need to deploy Flask inside an application server such as uWSGI, and uWSGI will in turn run inside an http server like nginx. uWSGI will manage the lifecycle of the parallel instances of Flask and propagate http requests from nginx to our app. The heavy lifting for this configuration task is already done for us in this family of Docker images provided by Sebastián Ramírez. This takes care of the basic uWSGI and nginx setup (we’ll address load balancing and SSL later) so all we need to do is build on what’s already been configured.

Docker has become an important part of Microsoft’s overall strategy but it seems like not all Microsoft developers are up-to-speed on it yet. There are many places where you can read about what a docker container is, so I won’t reproduce that here. Instead, I will give a quick introduction to the most important docker and docker-compose commands.

And—although we’ll be creating Docker containers from the Windows 10 command-line, Linux works much better inside Docker containers than Windows. As a result, all our python code will run inside a Linux container.

Set up Docker on Windows

Docker on Windows is easy to set up. Download it here. docker-compose makes life a little easier, so you may want to install that too.

Configuring Our App for Docker

There’s very little to configure to get our app into a Docker container. Here’s a Dockerfile that provides most of what we need:

# src/Dockerfile
# "FROM" starts us out from this Ubuntu-based image
# https://github.com/tiangolo/uwsgi-nginx-flask-docker/blob/master/python3.7/Dockerfile

FROM tiangolo/uwsgi-nginx-flask:python3.7

# Optionally, install some typical packages used for building and network debugging.
RUN apt-get update
RUN apt-get install -y  build-essential \
                        software-properties-common \
                        apt-transport-https \
                        build-essential \
                        ca-certificates \
                        checkinstall \
                        netcat \
                        iputils-ping

# Update to the latest PIP
RUN pip3 install --upgrade pip

# Uncommenting this will make rebuilding the image a little faster
# RUN pip3 install Flask==1.0.2 \
#                 flask-inputs==0.3.0 \
#                 jsonschema==3.0.1 \
#                 pytest==4.6.2

# Our application code will exist in the /app directory,
# so set the current working directory to that
WORKDIR /app

# Backup the default app files.  You could also delete these
RUN mkdir bak && \
    mv main.py uwsgi.ini bak


# Copy our files into the current working directory WORKDIR
COPY ./ ./

# install our dependencies
RUN  pip3 install -r requirements.txt

This Dockerfile is the recipe for creating the image that we’ll use to spawn a container for our app. The FROM line tells Docker to start from an image from Docker Hub that has versions of Python 3.7, Flask and uWSGI already installed. It then copies the relevant parts of our app to the /app directory. You can have a look at the original uwsgi-nginx Dockerfile And the derived flask Dockerfile if you want to know exactly what is installed. And the Dockerfile reference has more information on each of the instructions like COPY and RUN. But the part that might not be obvious is that the base images already contain a ENTRYPOINT which runs flask—we don’t need to override any startup commands ourselves.

I’ve added the basic Linux build tools into the image, plus some useful utilites for debugging—you could easily omit the apt-get lines if your python libraries don’t need to be built from source.

I also upgrade pip to the most recent version. You could also pre-install some of the updated python libraries that you’ll be using with pip—this saves some time when you’re running docker build (or docker-compose build) later.

The rest of the file creates our app in /app by moving the default main.py and config files into a directory called /bak, replacing them with our own, and then installing our python libraries from within our newly-created container.

We want to omit the intermediate files from being copied to the deployment version of the container, and we can do this by creating a file called src/.dockerignore:

# src/.dockerignore

**/__pycache__
**/*.py[cod]
**/*$py.class
**/*.pytest_cache

# C extensions
*.so

**/*.pkl

app/tmp
app/.gitignore
tmp/

Dockerfile*

Lastly, we will need the file src/uwsgi.ini that tells uWSGI how to start the flask server:

# src/uwsgi.ini 
[uwsgi]
module = app.main
callable = app

Build the Container

You can use plain docker to build the app and run it:

> docker build -t pythondemo .
> docker run --name demo -p 80:80 pythondemo 

Or you can store your configuration in a .yml file and use docker-compose instead. docker-compose requires a separate installation. It’s not really essential for a single container like this, but it allows you to save configuration in a file so that you don’t have to remember a profusion of docker parameters, and it will allow you to add more containers or mount volumes more easily later on.

> docker-compose build
> docker-compose up

# ... or do both
> docker-compose up --build

# then stop the container
> docker-compose down

To use docker-compose, first you need to create a simple docker-compose.yml file in the root directory (i.e. above src) like this:

version: '3'

services:

  demo:
    # this is the "repository" name.
    image: pythondemo
    # custom container name (rather than the generated default)
    container_name: pythondemo
    restart: unless-stopped
    build: src
    environment:
      FLASK_ENV: "development"
    ports:
      - "0.0.0.0:80:80"

Regardless, you should now have a running Linux container, and you should now be able to access your app locally at http://localhost.

The resulting image will be what gets deployed later in Kubernetes.

Useful Docker Commands

Now that you have a docker app running locally, try out some Docker commands from PowerShell:

  • docker images - lists the Docker images that are stored locally. You should see one named tiangolo/uwsgi-nginx-flask, as well as the image you just created.
  • docker ps - lists the running containers.
  • docker stop <name> - stops a running container
  • docker rm <name> - deletes a stopped container
  • docker rmi <name> - deletes an image

Note that if you don’t have a name for an image or a running container, you can also substitute part of the hash of the container id or image id. For example, if docker ps returns the following, you can type docker stop cd0a to stop the image:

> docker ps
CONTAINER ID   IMAGE    COMMAND                  CREATED        ...     
cd0afa472755   abc      "/entrypoint.sh /sta…"   40 minutes ago ...

Debugging a docker container

Sometimes the easiest way to debug your container is to log into it by starting a bash shell and poking around. This is easy to do for a running container:

docker exec -it <name> bash

So for our running container:

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                         NAMES
cd0afa472755        abc                 "/entrypoint.sh /sta…"   About a minute ago   Up About a minute   0.0.0.0:80->80/tcp, 443/tcp   demo

> docker exec -it demo bash
root@cd0afa472755:/app# ls

Dockerfile  app  bak  mypkg  prestart.sh  requirements.txt  tests  uwsgi.ini
root@cd0afa472755:/app#

Ctrl-D will close your shell, but keep your docker image running.

There’s a lot more to know about Docker, but this is enough to start deploying our pythondemo app on Azure Kubernetes Services in Part 4.