Anatomy of a Multi-Stage Docker Build
Wed 19 July 2017 by Moshe ZadkaDocker, in recent versions, has introduced multi-stage build. This allows separating the build environment from the runtime envrionment much more easily than before.
In order to demonstrate this, we will write a minimal Flask app and run it with Twisted using its WSGI support.
The Flask application itself is the smallest demo app, straight from any number of Flask tutorials:
# src/msbdemo/wsgi.py from flask import Flask app = Flask("msbdemo") @app.route("/") def hello(): return "If you are seeing this, the multi-stage build succeeded"
The setup.py
file,
similarly,
is the minimal one from any number of Python packaging tutorials:
import setuptools setuptools.setup( name='msbdemo', version='0.0.1', url='https://github.com/moshez/msbdemo', author='Moshe Zadka', author_email='zadka.moshe@gmail.com', packages=setuptools.find_packages(), install_requires=['flask'], )
The interesting stuff is in the Dockefile
.
It is interesting enough that we will go through it line by line:
FROM python:2.7.13
We start from a "fat" Python docker image -- one with the Python headers installed, and the ability to compile extensions.
RUN virtualenv /buildenv
We create a custom virtual environment for the build process.
RUN /buildenv/bin/pip install pex wheel
We install the build tools --
in this case, wheel
, which will let us build wheels,
and pex
, which will let us build single file executables.
RUN mkdir /wheels
We create a custom directory to put all of our wheels. Note that we will not install those wheels in this docker image.
COPY src /src
We copy our minimal Flask-based application's source code into the docker image.
RUN /buildenv/bin/pip wheel --no-binary :all: \ twisted /src \ --wheel-dir /wheels
We build the wheels.
We take care to manually build wheels ourselves,
since pex
, right now, cannot handle manylinux binary wheels.
RUN /buildenv/bin/pex --find-links /wheels --no-index \ twisted msbdemo -o /mnt/src/twist.pex -m twisted
We build the twisted
and msbdemo
wheels,
togther with any recursive dependencies,
into a Pex file -- a single file executable.
FROM python:2.7.13-slim
This is where the magic happens.
A second FROM
line starts a new docker image build.
The previous images are available --
but only inside this Dockerfile
--
for copying files from.
Luckily, we have a file ready to copy:
the output of the Pex build process.
COPY --from=0 /mnt/src/twist.pex /root
The --from=0
indicates copying from a previously built image,
rather than the so-called "build context".
In theory, any number of builds can take place in one Dockefile
.
While only the last one will actually result in a permanent image,
the others are all available as targets for --from
copying.
In practice, two stages are usually enough.
ENTRYPOINT ["/root/twist.pex", "web", "--wsgi", "msbdemo.wsgi.app", \ "--port", "tcp:80"]
Finally, we use Twisted as our WSGI container.
Since we bound the Pex file to the -m twisted
package execution,
all we need to is run the web
plugin,
ask it to run a wsgi
container,
and give it the logical (module) path to our WSGI app.
Using Docker multi-stage builds has allowed us to create a Docker container for production with:
- A smaller footprint (using the "slim" image as base)
- Few layers (only adding two layers to the base slim image)
The biggest benefit is that it let us do so with one Dockerfile, with no extra machinery.