Docker in CI: Testing Strategies
The "Ship" phase of the Docker lifecycle (File 04) relies heavily on automated testing. One of the greatest benefits of containerization is the ability to run tests against the exact same artifact that will be deployed to production.
This guide details the strategies for integrating Docker into your Continuous Integration (CI) pipeline to ensure code quality and reliability.
1. Linting the Dockerfile
Before you even build the image, you should validate the Dockerfile itself. This catches syntax errors and enforces best practices (like those in File 07).
Tool: Hadolint Hadolint is a smart Dockerfile linter that checks for common mistakes and violations of best practices.
How to use it in CI: Run Hadolint as a container against your local Dockerfile.
docker run --rm -i hadolint/hadolint < DockerfileCommon Errors it catches:
Using
latesttags (non-deterministic builds).Running
apt-get installwithoutapt-get updatein the same layer.Forgetting to clean up
aptcaches (bloated images).Installing packages without version pinning.
2. The Test Container Pattern
The most robust way to test is to run your test suite inside the container. This ensures that the environment where tests pass is identical to the production environment.
Strategy A: The Multi-Stage "Test" Target
Use a multistage Dockerfile to create a specific stage for testing.
# ... (Builder stage) ...
# --- Test Stage ---
FROM builder AS tester
# Install test dependencies (e.g., pytest, coverage)
RUN pip install pytest coverage
# Copy source code
COPY . .
# Run the tests
RUN pytest tests/
# ... (Production stage) ...In CI: You build up to the tester target. If the RUN pytest command fails, the build fails, and the pipeline stops.
docker build --target tester .Strategy B: The "Runner" Container
Build your production image, then use docker run to execute the test command inside it (overriding the default CMD or ENTRYPOINT).
In CI:
# 1. Build the image
docker build -t myapp:ci .
# 2. Run tests inside it
# We override the entrypoint to run our test runner
docker run --rm --entrypoint pytest myapp:ci tests/3. Integration Testing with Docker Compose
Unit tests are simple, but real applications rely on databases, caches, and other services. Docker Compose is the perfect tool for spinning up these ephemeral integration environments in CI.
The Goal:
Spin up a fresh database container.
Spin up your application container.
Run tests that make real HTTP requests or DB queries.
Tear everything down.
The docker-compose.test.yml File: Create a separate Compose file specifically for testing.
version: '3.8'
services:
# The application under test
sut: # System Under Test
build: .
command: pytest tests/ # Override command to run tests
depends_on:
- db
environment:
- DATABASE_URL=postgres://user:pass@db:5432/testdb
# Ephemeral database for testing
db:
image: postgres:14-alpine
environment:
- POSTGRES_DB=testdb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=passThe CI Workflow:
# 1. Build the images
docker-compose -f docker-compose.test.yml build
# 2. Run the test suite
# --abort-on-container-exit stops all containers when the 'sut' exits
# --exit-code-from sut makes the command return the exit code of the test runner
docker-compose -f docker-compose.test.yml up \
--abort-on-container-exit \
--exit-code-from sutSuccess: If tests pass,
sutexits with code 0. Compose tears down, and the CI job passes.Failure: If tests fail,
sutexits with code 1. Compose tears down, and the CI job fails.
4. Database Initialization in CI
A common "gotcha" in CI is that the application starts before the database is ready to accept connections.
The Problem: depends_on only waits for the container to start, not for the database process to be ready.
The Solution: Healthchecks or Wait Scripts
Option A: docker-compose Healthchecks (Compose v2.1+) Configure the db service with a healthcheck, and make sut depend on the condition service_healthy.
db:
image: postgres:14-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
sut:
depends_on:
db:
condition: service_healthyOption B: Wait-For-It Script (Universal) Wrap your test command with a script like wait-for-it.sh or dockerize.
sut:
command: ["./wait-for-it.sh", "db:5432", "--", "pytest", "tests/"]5. Scanning for Vulnerabilities
Security testing is part of CI. Before pushing an image to the registry, scan it.
Tool: Trivy Trivy is a comprehensive, easy-to-use vulnerability scanner.
In CI:
# Run Trivy container against your image
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image --exit-code 1 --severity CRITICAL myapp:ci--exit-code 1: Fails the pipeline if vulnerabilities are found.--severity CRITICAL: Only fails on critical issues (prevents noise).
