When writing Dockerfiles 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 
buildstage is shared betweenprodandtest, and it contains the app and its dependencies. - The 
teststage is on top ofbuildand also contains the tools neeeded for testing. It can also run the tests, to make every build implicitly tested. - The 
prodstage is also top ofbuild, but it also makes a “fake” reference totest, so that Docker runstestbeforeprod. (TheCOPYinstruction will not actually copy anything if the/tmp/dummypath 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?