Search across all documentation pages
Write a YAML test spec, then run Google's container-structure-test against the built image.
docker build -t app:ci .
container-structure-test test --image app:ci --config structure-test.yamlA realistic spec covers the three test families: metadata, files, and command output.
schemaVersion: 2.0.0
metadataTest:
user: "10001"
workdir: "/app"
exposedPorts: ["8080"]
env:
- key: NODE_ENV
value: production
entrypoint: ["node"]
cmd: ["dist/server.js"]
fileExistenceTests:
- name: app bundle present
path: /app/dist/server.js
shouldExist: true
- name: no shell in final image
path: /bin/sh
shouldExist: false
fileContentTests:
- name: CA certificates bundled
path: /etc/ssl/certs/ca-certificates.crt
shouldExist: true
commandTests:
- name: node is the expected major version
command: node
args: ["--version"]
expectedOutput: ["v22\\..*"]
- name: process user is non-root
command: id
args: ["-u"]
expectedOutput: ["10001"]Run it in CI right after the build and before push.
- name: Structure tests
run: |
docker build -t app:ci .
container-structure-test test --image app:ci \
--config structure-test.yamlThe tool loads the image, runs command tests inside an ephemeral container, inspects the filesystem for the file tests, and reads config metadata for the metadata tests.
Any failed assertion returns a non-zero exit code, which fails the pipeline.
metadataTest reads the image config directly, so it needs no running process.
It is the fastest way to assert user, exposedPorts, workdir, entrypoint, cmd, and env match your contract.
Asserting user: "10001" here catches a base image that silently reset the user back to root.
fileExistenceTests confirm required artifacts exist and forbidden ones do not - for example, that no shell ships in a distroless final stage.
fileContentTests go further and match file contents against a regex, which is useful for config files or CA bundles.
Both run against the image's layered filesystem without starting a process.
commandTests start a throwaway container, run a command, and match expectedOutput, excludedOutput, or expectedError against regexes.
Use them to verify a binary is on PATH, reports the right version, or that the effective UID is non-root.
Because they execute, keep them cheap - a version print, not a full server boot.
Pin schemaVersion: 2.0.0 so a tool upgrade does not silently change parsing.
Keep the spec in the repo next to the Dockerfile so changes are reviewed together.
Distroless and scratch images have no shell, so commandTests that rely on /bin/sh will fail - call binaries directly instead.
Command tests need a runnable entrypoint context; if your image sets a restrictive entrypoint, override it in the test command rather than fighting it.
exposedPorts asserts the EXPOSE metadata, not that anything listens - a smoke test is still required for real connectivity.
Regexes in expectedOutput are anchored loosely, so tighten them (for example ^10001$) when an exact match matters.
Running structure tests before the build, or against a stale tag, tests the wrong image - always build first and reference the fresh tag or digest.
Goss with dgoss runs assertions inside a live container, which overlaps with structure tests but leans toward runtime health checks.
A plain docker run plus shell assertions works for one-off checks but lacks the declarative, reviewable spec.
InSpec can profile a running container against compliance controls, which is heavier and aimed at audit rather than build-time gating.
Pick container-structure-test for fast, static, build-time contracts; pick Goss or a smoke test when you need to observe a running process.
Does it deploy to Kubernetes? No - it tests the image locally in CI, before it ever reaches a node running containerd.
Can it run without Docker? It supports the docker driver by default and can also test images via other drivers, but the Docker driver is the common CI path.
How is this different from linting? Linting reads the Dockerfile source; structure tests read the actual built image, catching drift the linter cannot see.
Should command tests boot my server? No - keep them to quick, deterministic commands; use a smoke test for real startup.
Where do I store the spec? In the repo beside the Dockerfile so it is versioned and reviewed with image changes.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026