Important: The template is proprietary software, licensed under the terms of the software license. The short version: you canât redistribute it outside your organization, e.g. to customers or in open source projects.
If you have any questions or problems please email meâbut do please first read this document in detail to make sure itâs not already covered.
In addition, while I aim to support many common features, not everything will be supported out of the box in the template. Some options:
Before you start Dockerizing your application, you can try it out without touching your code. In the template directory, run:
$ mkdir /tmp/python-docker-test
$ python3 install.py /tmp/python-docker-test
... say 'y' at the prompt ...
You now have a directory with everything needed to build a Docker image. To build an image called exampleapp
:
$ cd /tmp/python-docker-test
$ python3 docker-build/scripts/builder.py build exampleapp
Successfully finished building images:
exampleapp:nogit-build
exampleapp:nogit-runtime
By default two images are built: an intermediate image tagged with :<git-branch>-build
where everything is built, and a final, smaller image tagged with :<git-branch>-runtime
which is what youâll actually run in production. In this case, you donât have a Git branch, so instead it will use ânogitâ. To run the resulting image, then, youâll need to do:
$ docker container run --rm exampleapp:nogit-runtime
(You might need to sudo docker run
on Linux.)
If all goes well, you should see the default entrypoint output, telling you to edit docker-build/entrypoint.sh
and listing the installed packages.
Note: If you get the following problem:
File "docker-build/scripts/builder.py", line 2 SyntaxError: Non-ASCII character '\xe2' in file docker-build/scripts/builder.py on line 2, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details
Itâs because you used Python 2; make sure you use Python 3.7 or later.
Now itâs time to package your application!
You might think that a Dockerfile
is sufficient to build a good Docker image, but that is not the case.
So this template includes not just a Dockerfile
but also the necessary build infrastructure. At a high level, this template builds Docker images as follows:
There are actually two images built: one image used to compile and build your application, and a runtime image which is what you will run in production. This helps keep your runtime images small and more secure, since they donât include extraâand unnecessaryâthings like compilers. See this article for details.
Letâs copy the necessary files in to your applicationâs repository.
First, create a new branch in your version control.
Second, run the following command in the directory where you unpacked the template:
$ python3 install.py /path/to/your/app/repository/
$ cd /path/to/your/app/repository/
This will give you a list of the installed files. It will overwrite any existing files, which is why youâre doing this in a branch!
Third, commit the initial files to the branch.
By having an unmodified version committed to your repository, youâll have an easier time seeing what specific customizations you have made, and reverting back to the baseline if necessary.
Beyond this point youâll be editing files in your repository, not in the original template directory!*
Dockerfile
: The main configuration for the Docker image.By default the template using the python
Docker base images, pre-configured with a specific Python version. If you want to use a different Python version, you need to change the configuration.
If you want to use a different base image altogether, see the comments in the top of the Dockerfile
by the FROM
statement.
At the top of the Dockerfile
, change:
ARG PYTHON_VERSION="3.9"
to the version you want, e.g.:
ARG PYTHON_VERSION="3.10"
The template supports installing Python dependencies based on a number of different configuration mechanisms:
requirements.txt
file.pyproject.toml
+ poetry.lock
.Pipfile
+ Pipfile.lock
.setup.py
/setup.cfg
; this is not recommended since it will slow down rebuilding images. If that is your current mechanism, I recommend automatically creating a requirements.txt
from setup.py
using pip-tools
.pyproject.toml
-based build tools, like Flit.The template assumes these configuration files are in the root directory of your application. If you store these elsewhere, edit the Dockerfile
âs relevant COPY
file to point to the correct path.
First, you can check which of the two files will be used:
$ python3 docker-build/scripts/install-dependencies.py diagnose
Second, build the image, and then check which packages were installed. If youâre running off of a Git repository, replace yourbranch
below with the name of the current Git branch:
$ python3 docker-build/scripts/builder.py build exampleapp
Successfully finished building images:
exampleapp:yourbranch-build
exampleapp:yourbranch-runtime
$ docker run exampleapp:yourbranch-build pip list
$ docker run exampleapp:yourbranch-runtime pip list
Dockerfile
: The main configuration for the Docker image.By default the template assumes youâre running your code out of the directory where itâs installed, with no additional work. Some applications, however, expect that they will be installed via pip install
or poetry install
, and then run via the installed version.
If your application requires installation, rather than running out of the directory where itâs built, edit the top of Dockerfile
and change:
ARG RUN_IN_PLACE=1
To say:
ARG RUN_IN_PLACE=0
Dockerfile
: The main configuration for the Docker image.Sometimes you will need additional build steps in your Dockerfile
. For example:
Add those steps in the Dockerfile
, specifically where it says:
# If you need to run additional commands to ensure your code runs correctly, run
# them here.
#
# RUN python some-setup-script.py
To minimize cache invalidation, try to follow the pattern you already see in the Dockerfile
of first copying in just enough of the files to do the next build step. E.g. first copy in your package dependencies list, install those packages, and then in a later step do the actual build.
docker-build/entrypoint.sh
: The script that is run when someone runs your image via docker run
or your deployment system.When your Docker image is run (via docker run yourimage
or whatever your deployment environment is), it will run docker-build/entrypoint.sh
. So you will need to edit this file to ensure it runs the correct command.
The default script just prints some debug output, so youâll need to change it to run whatever command or commands your application needs to start. Make sure:
exec
so it exits cleanly.0.0.0.0
rather than 127.0.0.1
(see here for explanation).The script include a commented out example of a Gunicorn WSGI setup suitable for web applications.
You should now be able to run your image locally on your own computer, so itâs time to try it out and see if it works. If youâre running off of a Git repository, replace yourbranch
below with the name of the current Git branch:
$ python3 docker-build/scripts/builder.py build yourapp
$ docker run --rm yourapp:yourbranch-runtime
If it blows up, you can run the image using a shell as the entrypoint. You can then debug the problem in-place, e.g. try running the custom entrypoint script directly, install new packages with pip
(so long as they donât require a compiler), and in general play around until youâve figured out the issue.
If youâre running off of a Git repository, replace yourbranch
below with the name of the current Git branch.
$ docker run -it yourapp:yourbranch-runtime bash
appuser@25444adb5dc0:~$ which python
/usr/local/bin/python
appuser@25444adb5dc0:~$ bash /home/appuser/docker-build/entrypoint.sh
...
Notice that the applicationâs virtual environment is enabled by default, you donât need to change anything.
At this point you should be building images that work, so you can merge your temporary branch back into your main branch with a pull request or the equivalent.
Now that you have a working image, you probably want to set it up to build automatically whenever someone pushes a change to your version control repository.
At this point you have the image building locally, and so the next step is to make sure you have the credentials you need to push to an image registry, a server that stores images for you.
docker login
. Once you have those credentials, run docker login
appropriately on your local machine.Letâs assume youâre using Quay, in which case the name will be something like quay.io/yourorg/yourapp
. If your chosen name is already in use, test with a different image name, so you donât break your production images!
Next we run two commands, a build and a push, giving the image name:
$ export IMAGE_NAME=quay.io/yourorg/yourapp # <-- change appropriately
$ python3 docker-build/scripts/builder.py build $IMAGE_NAME
$ python3 docker-build/scripts/builder.py push $IMAGE_NAME
You can also push manually instead of using builder.py push
, just make sure you push both images. We want both images pushed so that when you automate builds (see below) you can get fast rebuilds. If youâre running off of a Git repository, replace yourbranch
below with the name of the current Git branch:
$ docker image push $IMAGE_NAME:yourbranch-build
$ docker image push $IMAGE_NAME:yourbranch-runtime
You should now see two images listed in the UI for your registry of choice; in this case quay.io/yourorg/yourapp:yourbranch-runtime
and quay.io/yourorg/yourapp:yourbranch-build
. You can change runtime
and build
to some other tag by editing docker-build/build_settings.py
appropriately.
You should also be able to pull the image:
$ docker pull $IMAGE_NAME:yourbranch-runtime
If this worked, the next step is making the above run in your CI/build system.
There are a number of situations where you likely want Docker images to be built automatically:
main
branch or equivalent.See this article for a detailed explanation of why you want the latter.
Copy .github/workflows/docker.yml
into your own repositoryâs .github/workflows/
directory.
By default this uses GitHubâs Container Registry. You can change that to any other container registry by editing the fields for the docker/login-action
: the username
, password
, and registry
fields.
You will also need to customize the environment variable called BUILD_IMAGE_NAME
in the docker.yml
to match complete image name youâre building, including the registry, e.g. ghcr.io/yourorg/yourimage
.
By default the main
/master
branch and pull-requests to that branch have images built, as well as tags; if you donât want that, youâll need to edit docker.yml
appropriately.
Copy the configuration from .gitlab-ci.yml
into your own configuration.
By default GitLabâs built-in Docker image registry is used, but if you want to push to another registry you can change the configuration appropriately.
IMPORTANT: The included configuration will reuse cached layers indefinitely, which means you will not get security updates (see this article). To fix that, you will need to manually set up a weekly build that rebuilds without caching:
EXTRA_BUILD_ARGS
should be set to --no-cache
.For instructions on setting up scheduled pipelines see the documentation.
By default the main
/master
branch and pull-requests to that branch have images built, as well as tags; if you donât want that, youâll need to edit .gitlab-ci.yml
appropriately.
If you have some other build or CI system, you will need to run a build and push script manually in your CI system.
For example, if your image is quay.io/yourorg/yourapp
, you want a build script that looks something like this:
#!/bin/bash
set -euo pipefail # bash strict mode
docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD quay.io
export BUILD_IMAGE_NAME=quay.io/yourorg/yourapp
python3 docker-build/scripts/builder.py build $BUILD_IMAGE_NAME
python3 docker-build/scripts/builder.py push $BUILD_IMAGE_NAME
To rebuild images from scratch without caching (which you should do weekly or daily to get security updates) you can run docker-build/scripts/builder.py build --no-cache yourimagename
.
Create a pull request to your main branch, and see if a Docker image gets built and pushed automatically.
Additional configuration will allow you to make sure you donât push broken images, and that your images donât include unnecessary files.
Before new images get pushed to the registry, itâs useful to run minimal test to ensure the new image is not completely broken. For example, a webserver can be tested by a sending a simple HTTP query to a running container.
Itâs worth while implementing such a test, and then adding it to your CI config right before the command that pushes to the registry. For more details see this article on Docker image smoke tests.
.dockerignore
and COPY
so you donât package unnecessary files.dockerignore
: Lists files that wonât get copied into the Docker image.Dockerfile
: The COPY . .
line..dockerignore
lists files that shouldnât be copied into the Docker image. If you have any large data files in the repository, or secrets, or any other files that shouldnât be copied into the Docker image, add them here.
The file format is documented here.
Additionally, you can edit the Dockerfile
so instead of just copying everything in the current directory (as filtered through .dockerignore
) it only copies the files you need. For example, if you only need the yourpackage/
and data/
directories, you can change the âCOPY . .
â line to "COPY yourpackage/ data/ ./
.
dive
is a tool for figuring out whatâs in your image, and where itâs coming from.
docker-show-context
is another useful utility that lists which large files made it into the Docker context. You can use it to figure out if there are any large files being copied in.
The build will check the size of the runtime image, and complain if itâs too large. This will help catch unexpectedly large images. To change the configured maximum, edit MAX_IMAGE_SIZE_MB
in docker-build/build_settings.py
.
If youâre doing local development, you might want to build the image locally and have it install development dependencies like black
or flake8
. You can do so by using the --dev
option; for example, this will build a Docker image called yourapp
with development dependencies installed:
$ ./docker-build/scripts/builder.py build --dev yourapp
The template supports this in three ways:
requirements.txt
to install dependencies, you can provide a dev-requirements.txt
.If youâre building manually, you can do:
$ docker build --build-arg INSTALL_DEV_DEPENDENCIES=1 .
And if youâre using Docker Compose, your docker-compose.yaml can do:
version: "3.9"
services:
main:
context: .
args:
INSTALL_DEV_DEPENDENCIES: "1"
By default images will be labeled with the current Git branch or tag, and the current git commit. You can see this metadata by running:
$ docker image inspect yourimage:yourtag | grep git
If you want to add more labels, add them to EXTRA_CLI_BUILD_ARGS
in docker-build/build_settings.py
.
By default, the Docker images are tagged based on the current Git branch or tag. If youâd like to customize this behavior, youâll want to modify the various relevant options in docker-build/build_settings.py
.
docker build
If you want to add additional arguments to docker build
(see the CLI docs), you can add them via the EXTRA_CLI_BUILD_ARGS
list in build_settings.py
. For example, if you want to run docker build
with --secret id=MYSECRET,src=secret.txt
you can do:
EXTRA_CLI_BUILD_ARGS.extend(['--secret',
'id=MYSECRET,src=secret.txt',
])
The Dockerfile
should work out of the box with Compose, with the caveat that if you use EXTRA_CLI_BUILD_ARGS
in the build_settings.py
config, you will need to add those arguments to the docker-compose.yml
as well.
If you want to have Compose build the image, you can have the following docker-compose.yml
:
version: "3.9"
services:
main:
build:
context: .
Note that docker-compose up
doesnât automatically rebuild the image when the code changes; youâll need to do docker-compose up --build
.
Also note that you may get faster builds on Linux if you enable BuildKit (on macOS and Windows Buildkit is enabled by default):
$ export DOCKER_BUILDKIT=1
By default the template uses BuildKit, which results in faster builds and supports additional features like build secrets.
Every application really requires two different sets of dependency descriptions:
The first set of dependencies can be used to easily update the second set of dependencies when you want to upgrade (e.g. to get security updates).
The second set of dependencies is what you should use to build the application, in order to get reproducible builds: that is, to ensure each build will have the exact same dependencies installed as the previous build.
Some tools that do this are pipenv
and poetry
, but the easiest way to do that is with pip-tools
. Since pip
install dependencies based on your current operating system, Iâve written a little script that runs pip-tools
inside Docker so you get Linux-specific dependencies.
Because of the use of caching, system packages wonât get security updates by default. This is why the default CI configuration above makes sure to rebuild the image from scratch, without any caching, once a week. Note that on GitLab CI this requires some manual setup.
Make sure you have set this up, otherwise you will eventually end up with insecure images.
You will then want to deploy these updates images to your production environment.
If you are using pinned dependencies, you will need an ongoing process to re-pin to new versions in order to get security updates and critical bugfixes.
GitHub can automatically notify you of security updates and can also update your dependencies in general.
There are also third-party services like requires.io, PyUp, and others that will scan your dependencies for vulnerabilities.
You can and should define health checks for a Docker imageâa way for whatever system is running the container to check if the application is functioning correctly. The Docker image format itself supports defining health checks, however some systems like Kubernetes ignore these and have their own way of specifying these.
So check the documentation for the systems where you run your images, and add health checks.
The template is licensed using the attached license. Essentially, you canât distribute the template code to any other organization, and you may only be able to package a limited number of services depending on your purchase. The only exceptions are the entrypoint.sh
and activate.sh
, which you can distribute as you wish (the latter is based on open source code, see the file for details).
What you can do:
If you have any questions or problems please email me.
poetry
instead of pip
, which makes supporting third-party package repositories easier.To upgrade, copy in the updated docker-build/scripts/install-dependencies.py
script into your repository.
Compared to version 1.x, the configuration has been simplified and is more template-like. In addition:
Upgrading from 1.0 is probably easiest by just by starting from scratch. If the 1.0 template is working for your application, there is however no pressing need to upgrade.