I love Python. I think it's a beautifully designed language with a philosophy that I really appreciate as a developer trying to get stuff done. Just run this at a command prompt: python -m this
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
What's not so nice is Python packaging. It's awkward, it's confusing - it's getting better but it's still not nearly as nice as it is in other languages (Ruby, Java, Javascript).
Docker is a collection of various Linux features - namespaces, cgroups, union file-system - put together in such a way that you can package and distribute software in a language-agnostic container. Docker is a great way to skirt the pain of Python packaging.
To install it, go to https://www.docker.com/ and under the "Get Docker" link choose the version for your operating system.
Just Enough Docker
So. What's the least we can get away with? Or, what's the least I can write to illustrate this? Well, if we use the onbuild
Python Docker image, then not much.
Imagine we have a super-simple Flask app which just has one route, returning a fixed string (Hello, World?). We need very little, but we do have the dependency on Flask. Even on my Mac, with the latest OS (OK, not High Sierra just yet), the default Python installation doesn't include the pip
package manager. What the hell? It does include easy_install
, so I could easy_install pip
, or rather sudo easy_install pip
because it'll go in a global location. Then I could globally install the flask
package too. Woop-de-doo. Which version is now globally installed on my system? Who knows!(?)
Let's not do that. Let's create our requirements.txt
file:
echo Flask > requirements.txt
And our Flask app:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello, World!'
if __name__ == '__main__':
app.run(host='0.0.0.0')
And then let's have a Dockerfile
too:
echo FROM python:onbuild >> Dockerfile
Build It
OK, so let's build it:
$ docker build -t myapp.local .
Sending build context to Docker daemon 4.096kB
Step 1/1 : FROM python:onbuild
# Executing 3 build triggers...
Step 1/1 : COPY requirements.txt /usr/src/app/
---> Using cache
Step 1/1 : RUN pip install --no-cache-dir -r requirements.txt
---> Using cache
Step 1/1 : COPY . /usr/src/app
---> Using cache
---> 79fdf87107de
Successfully built 79fdf87107de
Successfully tagged myapp.local:latest
Was that it? Is it built? Yup:
$ docker image ls myapp.local
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp.local latest c58ad169cb28 4 seconds ago 700MB
Run It
Great, our app is inside a container! What next? Run the container like this:
$ docker run --rm myapp.local python server.py
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
That tells the docker
command to run
the myapp.local
container (which we built), to remove it when it stops (--rm
) and to run the command python server.py
inside it. Amazing! So can we see our app now?
The Final Piece
We can't see our app at the moment because although it's running perfectly inside the container it's not accessible anywhere else. The message we get when we run it says it's listening on port 5000
, but if we try to access that on the same host we get an error:
$ curl localhost:5000
curl: (7) Failed connect to localhost:5000; Connection refused
We need to expose the port outside the container that it's running in:
docker run --rm -p 5001:5000 myapp.local python server.py
The -p
argument maps your local 5001
port to 5000
inside the container (they can just be the same but I've made them different just to illustrate where the host and container ones are).
OK, not quite the end...
So I found out when I was writing this that:
The ONBUILD image variants are deprecated, and their usage is discouraged.
That's OK - you wouldn't really use the ONBUILD
image for anything serious, and the Dockerfile
that defines it is very easy to understand. Check it out for yourself and see how it works: https://github.com/docker-library/python/blob/f12c2d/3.6/jessie/onbuild/Dockerfile
We can replace the contents of our Dockerfile
with the following and get the same result:
FROM python
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r requirements.txt
COPY . /usr/src/app
I've just removed the ONBUILD
directives (plus the 3.6-jessie
Python version - we can just take the latest).
Really The End
So that's our minimal example of how to run a Python app with its dependencies inside a Docker container. Docker is an amazing piece of technology and it's no surprise that the company is valued so highly. In this instance it provides a clean way for us to avoid pain with Python packaging, but we now also have a container image with our app inside which could be distributed and run on any other system which can run Docker.
Top comments (2)
For this simple app you might want to check how much disk space it using now. It's a bit crazy to me. Unfortunately, I still can't get the minimal image as in this blog post working.
Yeah, the overhead of the operating system adds a lot that you don't really need in a container.
Switching to an Alpine based image shaves off a lot i.e. just using
FROM python:alpine
instead ofFROM python
at the top of the file saves over 600MB: