This post covers the steps to create a simple dockerised flask app to cover some of the basic steps required when creating a REST(ish) service that can be run as a Docker container.
The App
Rather than go with the obvious “Hello, World!” type example, I decided I’d try and do something just a touch more interesting and create a REST(ish) resource that will return a response with the status code that was passed in the path of the request. This might be useful for test frameworks where you want to validate some codes reaction to a given response status code or similar.
I’m using Flask to create a quick an dirty solution, mostly to keep it simple. Firstly, the requirements.txt
file is simple, just one requirement;
flask
requirements.txt
This will get us the Flask package to use in our simple REST(ish) service, which is essentially this; (forgive the inline status_codes
dict)
import json
from flask import Flask, Response
status_codes = {
"100": "Continue",
"101": "Switching Protocols",
"102": "Processing",
"103": "Early Hints",
"200": "OK",
"201": "Created",
"202": "Accepted",
"203": "Non-Authoritative Information",
"204": "No Content",
"205": "Reset Content",
"206": "Partial Content",
"207": "Multi-Status",
"208": "Already Reported",
"226": "IM Used",
"300": "Multiple Choices",
"301": "Moved Permanently",
"302": "Found",
"303": "See Other",
"304": "Not Modified",
"305": "Use Proxy",
"307": "Temporary Redirect",
"308": "Permanent Redirect",
"400": "Bad Request",
"401": "Unauthorized",
"402": "Payment Required",
"403": "Forbidden",
"404": "Not Found",
"405": "Method Not Allowed",
"406": "Not Acceptable",
"407": "Proxy Authentication Required",
"408": "Request Timeout",
"409": "Conflict",
"410": "Gone",
"411": "Length Required",
"412": "Precondition Failed",
"413": "Payload Too Large",
"414": "URI Too Long",
"415": "Unsupported Media Type",
"416": "Range Not Satisfiable",
"417": "Expectation Failed",
"421": "Misdirected Request",
"422": "Unprocessable Entity",
"423": "Locked",
"424": "Failed Dependency",
"425": "Too Early",
"426": "Upgrade Required",
"428": "Precondition Required",
"429": "Too Many Requests",
"431": "Request Header Fields Too Large",
"451": "Unavailable For Legal Reasons",
"500": "Internal Server Error",
"501": "Not Implemented",
"502": "Bad Gateway",
"503": "Service Unavailable",
"504": "Gateway Timeout",
"505": "HTTP Version Not Supported",
"506": "Variant Also Negotiates",
"507": "Insufficient Storage",
"508": "Loop Detected",
"510": "Not Extended",
"511": "Network Authentication Required"
}
app = Flask(__name__)
@app.route('/<code>', methods=['GET', 'POST', 'HEAD', 'PUT'])
def status_code(code):
message = status_codes.get(code, "Unknown Status Code")
return Response(status=int(code), response=message)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
app.py
You can test the code by running python app.py
which will launch the app on port 5000. Quick test might be;
curl -v http://localhost:5000/405
All being well, this will give you a response of
* Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 80 failed: Connection refused
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET /405 HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 405 METHOD NOT ALLOWED
< Content-Type: text/html; charset=utf-8
< Content-Length: 18
< Server: Werkzeug/0.14.1 Python/3.7.2
< Date: Sat, 19 Jan 2019 16:26:34 GMT
<
* Closing connection 0
Method Not Allowed%
NOTE, the response status is the code that we’ve passed HTTP/1.0 405 METHOD NOT ALLOWED
Running it as a Docker container
Installing Docker
First, you’re going to need to have Docker on your machine. Best approach is going to be downloading the Docker Desktop for your particular machine.
Creating the DockerFile
Dockerfiles require a base image to start from, for a lightweight Python container we can just use the Alpine image to derive our container. This image is a minimal Docker image which is only 5mb in size. You can learn more about Alpine here
The Dockerfile below is all that we’re going to need. It assumes the basic file structure of the project is similar to the tree below;
.
├── Dockerfile
├── app
│ ├── __init__.py
│ └── app.py
└── requirements.txt
We’ve covered app.py
, requirements.txt
and __init__.py
is an empty file. All thats left is the Dockerfile
FROM python:alpine
EXPOSE 5000
# Copy over the application
WORKDIR /app
COPY . /app
RUN python3 -m pip install -r requirements.txt
# Start the application
CMD ["python3", "app/app.py"]
Dockerfile
Breaking this down we’re saying that our image is
- going to be based on the
python:alpine
image. - going to expose something on port 5000 (in this case the app)
- going to use /app as its working directory
- going to copy the contents of
app
to the/app
folder on the image - going to install the requirements as specified in
requirements.txt
Finally, we end with the CMD
which specifies what will happen when the container starts. In this case, we’re going to be starting the Flask app.
Building the docker file
We need to build the image to be able to use it. This is assuming you’ve installed and started Docker on your machine.
To build the image we used the docker build
command.
docker build . -t httpcodes:latest
This will give an output with the steps that are performed while building the image
Sending build context to Docker daemon 11.78kB
Step 1/7 : FROM python:alpine
---> 1a8edcb29ce4
Step 2/7 : LABEL Name=docker Version=0.0.1
---> Using cache
---> 2076201409c8
Step 3/7 : EXPOSE 3000
---> Using cache
---> 63588eaed844
Step 4/7 : WORKDIR /app
---> Using cache
---> feb03f342d39
Step 5/7 : ADD . /app
---> Using cache
---> fe2d365303a5
Step 6/7 : RUN python3 -m pip install -r requirements.txt
---> Using cache
---> b3ecdb9890ad
Step 7/7 : CMD ["python3", "app/app.py"]
---> Using cache
---> 1616f252e49d
Successfully built 1616f252e49d
Successfully tagged httpcodes:latest
We can now run the image
docker run -d -p 80:5000 httpcodes
This command is telling Docker to start a container based on the httpcodes (infering latest because no version was specified) and to do a port forward from the host (your machine) to port 5000 on the container. In this case, we’re saying route all traffic that comes to http://localhost:80 to 5000 on the container.
Testing the endpoint
As before, we can test the endpoint to make sure it does as we expected.
curl -v http://localhost/405
All being well, this will give you a response of
* Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 80 failed: Connection refused
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /405 HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 405 METHOD NOT ALLOWED
< Content-Type: text/html; charset=utf-8
< Content-Length: 18
< Server: Werkzeug/0.14.1 Python/3.7.2
< Date: Sat, 19 Jan 2019 16:26:34 GMT
<
* Closing connection 0
Method Not Allowed%