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 namedtiangolo/uwsgi-nginx-flask
, as well as the image you just created.docker ps
- lists the running containers.docker stop <name>
- stops a running containerdocker rm <name>
- deletes a stopped containerdocker 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.