Should I test that?

Should I run tests in the Docker build?

Verdict

Yes

Yes, run the unit tests during the Docker build, in a test stage that installs the same runtime and packages as the image you ship; make CI build that stage, and run the integration tests against the finished image before you push it.

Why

Yes, run the unit tests in the Docker build, in a test stage that installs the same runtime and packages as the image you ship; rule R11 gives Test for the service those tests cover. The typical case is a backend service that CI ships as a Docker image. Blast radius is users, because customers call the service, and Change frequency is regularly, because the code and its packages change about once a month. Detectability is eventually, because a bug that returns plausible data raises no error, and Reversibility is with-effort, because a redeploy leaves behind the data the faulty version wrote. Test cost is moderate: the tests exist, and a test stage takes about an hour to add.

When the decision changes
WhenDecisionWhy
The service in the image charges customers' cardsTest mandatory: the test stage covers every charge path and boundary amount, reviewed by a second personBlast radius rises to money and Reversibility to costly, because a wrong charge needs refunds
The service returns records of several customer companies from one databaseTest mandatory: after the build, test the image against a database with rows of two companiesBlast radius rises to safety-or-legal and Detectability to never, because a missing company filter shows another company's records without an error
CI tests a static Go binary on the runner, and the Dockerfile copies that same file into a distroless imageDo not run tests in the Docker build; the deploy's health check rejects an image that does not startDetectability moves to immediately and Reversibility to trivial, because the image adds no code to the tested binary and a missing file stops the container at start
The image runs an admin tool that only staff use, with no customer data or paymentsTest minimally: the test stage runs one test of the main pathBlast radius falls to internal, because a failure slows staff and no customer sees it
The image runs a job once, to backfill a new column in customer recordsTest it differently: run the image on a restored copy of production data and compare row counts before the real runChange frequency falls to once, so a test stage would guard a build that never repeats

What breaks if you don't test

Tests that run only on the CI runner use the runner's runtime and packages, while the Dockerfile installs its own. When the two differ, such as Python 3.12 on the runner and python:3.11-slim in the Dockerfile, or a lock file on the runner and an unpinned pip install in the image, you push a combination no test ran. A module that fails to import stops the container at start. A package version that parses dates differently returns plausible wrong values that customers find weeks later.

What you lose if you over-test

Integration tests inside the build need --network=host and a database already running on the build machine, a setup that breaks when the runner changes. Tests left in the release image ship pytest and its plugins to production, and each vulnerability found in them counts against that image.

How to test

Split the Dockerfile into stages, as the multi-stage build guide shows:

  1. A base stage installs the runtime and the locked production packages, then copies the source.
  2. A test stage starts from base, adds the test packages and tests, and runs the unit tests.
  3. The release stage starts from base and copies a marker file from the test stage, so no build skips the tests.
  4. After the build, start the image with its database in Docker Compose, run the integration tests against it, and push only then, as in Docker's test-before-push workflow.

When the answer changes

  • The Dockerfile only copies a binary that CI already tested.
  • The service charges money or holds several customers' data.
  • The image runs one job, once.

Real incident + Code example

The test stage that BuildKit skipped

On a Python API I worked on, the Dockerfile's test stage ran pytest, and the release stage started from the base stage. CI ran only docker build -t api .. Docker Engine 23.0 made BuildKit the default builder on Linux, and BuildKit builds only the stages the target depends on. After the runners were upgraded, the test stage stopped running and every build stayed green. Five weeks later a search filter bug reached customers; its test had been failing for nine days, and nobody ran the suite outside CI. The release stage now copies a file that exists only when pytest passes:

# syntax=docker/dockerfile:1
FROM python:3.12-slim AS base
WORKDIR /app
COPY requirements.lock .
RUN pip install --no-cache-dir -r requirements.lock
COPY src/ src/

FROM base AS test
COPY requirements-dev.lock .
RUN pip install --no-cache-dir -r requirements-dev.lock
COPY tests/ tests/
RUN pytest -q && touch /tmp/tests-passed

FROM base AS release
# BuildKit skips stages the target does not use; this copy makes release use test.
COPY --from=test /tmp/tests-passed /tmp/tests-passed
USER nobody
CMD ["python", "-m", "src.api"]

FAQ

Should you run unit tests in a Dockerfile?

Yes, run unit tests in a separate test stage of a multi-stage Dockerfile that starts from the same stage as the image you ship. The tests then run on the image's runtime and packages.

Should I include tests in the Docker image?

No, keep test code and test packages out of the image you push. Put test code in a test stage that the release stage does not start from.

Should integration tests run inside docker build?

No, run integration tests after the build, against the image and its database in Docker Compose. A RUN step in docker build cannot start a database container next to it.

Why did my Docker test stage stop running?

BuildKit, the default builder since Docker Engine 23.0, skips every stage that the target stage does not depend on, including a test stage that the release stage never uses. Build it with --target test in CI, or copy a file from it into the release stage.