When writing Dockerfile
s for development and deployment there is a
tension between keeping them well tested and lean. I would ideally want to
run the tests on in the same environment as the production app. But I
don’t want to install all tests and their dependencies into the production
container.
I can get close to this goal with a pattern that uses multiple Docker stages.
Pre-requisites: multi-stage builds.
The general simple form
I want to have a two Dockerfile
with two “flavors”, one for the
production container, and one for the one with the tests. The second is
based on the first one, but it has additional tools to run the the tests.
I can implement this with a single Dockerfile
and three stages:
1 | ARG BASE=<base-image> |
- The
build
stage is shared betweenprod
andtest
, and it contains the app and its dependencies. - The
test
stage is on top ofbuild
and also contains the tools neeeded for testing. It can also run the tests, to make every build implicitly tested. - The
prod
stage is also top ofbuild
, but it also makes a “fake” reference totest
, so that Docker runstest
beforeprod
. (TheCOPY
instruction will not actually copy anything if the/tmp/dummy
path doesn’t exist.)
The last stage is the default target, docker build
builds prod
by
default.
For R projects
DESCRITPION
+ pak
For an R project, that use DESCRIPTION
to define dependencies and pak to
install them, this could look:
1 | ARG BASE=ghcr.io/r-lib/rig/ubuntu-22.04-release |
renv
If I am using renv to define my R package dependencies, then I create a
test
profile which includes the dependencies for running the tests as
well, and then I can use this Dockerfile
:
1 | ARG BASE=ghcr.io/r-lib/rig/ubuntu-22.04-release |
Adding a dev stage
Often I also want another stage, that has extra packages for development:
devtools, usethis, etc. The dev stage should also have the dependencies
for the tests, but I don’t actually want to run the tests when building
the dev
stage. The general Dockerfile
could look like this:
1 | ARG BASE=<base-image> |
This Dockerfile
builds the dev
stage by default. I have to request
the prod
stage explicitly:
1 | docker build --target prod . |
For an R project with pak it could look like this:
1 | ARG BASE=ghcr.io/r-lib/rig/ubuntu-22.04-release |
Leaving out the test files
Sometimes I include test fixtures in the project, inside tests/
, and I
want to leave tests/
out from the prod
containers, while I need to
include them in the test
containers. COPY
cannot exclude paths
currently, but BuildKit can now use Dockerfile
syntax
extensions, and the docker/dockerfile:1.7.0-labs
extension supports excluding paths. Newer Docker installations use BuildKit
by default.
The updated Dockerfile
that leaves out tests/
from the prod
containers could look like this:
1 | # syntax=docker/dockerfile:1.7-labs |
The first line defines the extended Dockerfile
syntax. Then I can use
COPY --exclude=...
later to exclude tests
from the prode
image, and
COPY tests
only in the test
stage separately.
Comments
What do you think?