You’ve conducted your research, analysed the data, built models, and packaged everything up in a user friendly application that is ready to be shared and published within your research community and beyond.
There’s just one problem. Whilst your models and application run fine on your own development machine, perhaps this machine is running on an older University Windows Server, or you’ve collected and installed various packages and code libraries over time, set environment variables, configurations, etc etc. How can someone re-create your environment to run and use all of your hard work?
Containers solve this problem by packaging up all of the software, settings and code used to execute and application within a single and sharable Docker Container. In addition, containerization enables a wide variety of different operating systems and environments to be used from a single underlying host’s operating system and infrastructure.
This post demonstrates how to setup a simple Docker container to run an API using Python Flask. The code is avaliable in this repo.
Install Docker
These instructions are for Windows 10 OS. See these instructions for Ubunut/Linux Mint 19 installation. Create an account, download and install Docker Desktop for your operating system following the official channel. Once installed you check some basics using the following commands. See the git repo readme and official docs for more.
1
2
3
4
5
6
7
8
# print docker version info
$ docker -v
# list images
$ docker images
# list containers
$ docker ps -a
Create a project directory
Our app has a simple structure.
1
2
3
4
5
6
7
├── conda-flask-api
│ ├── start.ps1 <- Windows powershell to build and run our docker
│ ├── Dockerfile <- Instructions to build our container
│ ├── environment.yml <- Conda environment.yml
│ ├── serve.sh <- bash script to run an application server
│ └── raw
│ └── flask-api.py <- the flask app
There are few new files and terms in here that are explained below.
flask-api.py
Flask is a lightweight framework for building web applications. If you’re new check out the quickstart.
Lets define some requirements for our app:
- at root, print some information about the API and show a valid sample request.
- return the square of the “value” supplied by the user.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# flask-app.py
from flask import Flask, request
import json
# create a Flask instance
app = Flask(__name__)
# a simple description of the API written in html.
# Flask can print and return raw text to the browser.
# This enables html, json, etc.
description = """
<!DOCTYPE html>
<head>
<title>API Landing</title>
</head>
<body>
<h3>A simple API using Flask</h3>
<a href="http://localhost:5000/api?value=2">sample request</a>
</body>
"""
# Routes refer to url'
# our root url '/' will show our html description
@app.route('/', methods=['GET'])
def hello_world():
# return a html format string that is rendered in the browser
return description
# our '/api' url
# requires user integer argument: value
# returns error message if wrong arguments are passed.
@app.route('/api', methods=['GET'])
def square():
if not all(k in request.args for k in (["value"])):
# we can also print dynamically
# using python f strings and with
# html elements such as line breaks (<br>)
error_message = f"\
Required paremeters : 'value'<br>\
Supplied paremeters : {[k for k in request.args]}\
"
return error_message
else:
# assign and cast variable to int
value = int(request.args['value'])
# or use the built in get method and assign a type
# http://werkzeug.palletsprojects.com/en/0.15.x/datastructures/#werkzeug.datastructures.MultiDict.get
value = request.args.get('value', type=int)
return json.dumps({"Value Squared" : value**2})
if __name__ == "__main__":
# for debugging locally
# app.run(debug=True, host='0.0.0.0',port=5000)
# for production
app.run(host='0.0.0.0', port=5000)
You can run your app directly in several ways:
- directly in python. allows debugging.
1
2
3
4
5
$python flask-api.py
* Serving Flask app "flask-api" (lazy loading)```
2. or using Flask's builtin server.
$ export FLASK_APP=flask-api.py $ flask run
- Running on http://0.0.0.0:5000/ ```
serve.sh
Our app runs fine locally for debugging but this wont fly in production. Web applications generally require:
- A web server (like nginx). The web server accepts requests, takes care of general domain logic and takes care of handling https connections.
- A WSGI application server (like Gunicorn). The application server handles the requests which are meant to arrive at the application itself.
- The application.
Here we are only going to worry about our Application Server and our App. This post isn’t intended to explain web servers and requests, however see this SO answer if you’re interested and want to learn more.
For the Application Server we will use Gunicorn. It is more robust than Flasks internal debugging server we used above in that it:
- host files
- handles conncetions
- manages server errors and issues
- improves scalability
Here’s the script that run’s our flask-api app in gunicorn. The arguments: change source directory to ‘/app’, bind a server socket ‘5000’, and assign our “flask-api” as the generic “app”.
1
2
3
4
# serve.sh
#!/bin/bash
# run with gunicorn (http://docs.gunicorn.org/en/stable/run.html#gunicorn)
exec gunicorn --chdir app -b :5000 flask-api:app
environment.yml
Nothing fancy here i’m using Conda but you could also use pip virtualenv.
1
2
3
4
5
6
7
name: base
channels:
- defaults
dependencies:
- python=3.7
- flask
- gunicorn
Dockerfile
This is the file that we pass to Docker and lists the instructions used to build and execute our container. In a similar fashion to Git, Docker Hub hosts official and community developed Docker images for popular operating systems and deployments. Here we use a debian/miniconda environment from contiuumio. You can even check out the Dockerfile to see how this image itself is built.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# pull the image from docker hub
FROM continuumio/miniconda3:latest
# adds metadata to an image
LABEL MAINTAINER="Ben Postance"
LABEL GitHub="https://github.com/bpostance/deng.learn/tree/master/docker"
LABEL version="0.0"
LABEL description="A Docker container to serve a simple Python Flask API"
## Override the default shell (not supported on older docker, prepend /bin/bash -c )
SHELL ["/bin/bash", "-c"]
# Set WORKDIR - the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile
WORKDIR /home/flask-api
# COPY - copies files or directories from <src> and adds them to the filesystem of the container at the path <dest>.
COPY environment.yml ./
# ADD - "adds" directories and their contents to the container
ADD app ./app
# chmod - modifies the boot.sh file so it can be recognized as an executable file.
COPY serve.sh ./
RUN chmod +x serve.sh
# conda set-config and create environment based on .yml
# chain seperate multi-line commands using '&& \'
RUN conda env update -f environment.yml
# set env variables
RUN echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc && \
echo "conda activate" >> ~/.bashrc
# EXPOSE - informs Docker that the container listens on the specified network ports at runtime
EXPOSE 5000
# ENTRYPOINT - allows you to configure a container that will run as an executable.
ENTRYPOINT ["./serve.sh"]
Build & Run docker
This last file brings everything together in Docker.
To run this file you will need to be within the project root /conda-flask-api. First, docker build and tag your image. The standard format is “type/name:version”. The “.” references the “./Dockerfile”.
When you run Docker build docker will print step by step information and raise any issues in the terminal. When getting started it can be helpful to add additional prints to see exactly what docker is doing e.g. “RUN pwd”, “RUN ls” etc.
1
2
# docker build
docker build -t demo/flask-api:0.0 .
and to run the container:
1
2
3
4
5
6
# docker run
# --name assign name for ease of reference
# -d to run in detached mode
# -p to bind container:local ports
# tag of the container to run
docker run --name demo-flask-api -d -p 5000:5000 demo/flask-api:0.0
Now when you inspect running dockers you will see your container.
1
2
3
4
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6d4ea8c141df demo/flask-api:0.1 "./serve.sh" 52 seconds ago Up 51 seconds 0.0.0.0:5000->5000/tcp demo-flask-api
And visit http://localhost:5000/ or http://localhost:5000/api?value=2 to visit your api.
That’s all folks
I am writing a follow up post that explains how to use docker-compose to create multi-container applications.
Thank you for reading.
Ben Postance
1: https://www.docker.com/resources/what-container