A production-ready Dockerfile should do more than start a process: it should build predictably, use cache effectively, contain only what the application needs, and run with an appropriate security posture. This checklist explains how to optimize Docker image size, structure multi-stage builds, inspect image contents, handle processes correctly, and review images before they enter a CI/CD pipeline.
Overview
Docker image quality is shaped primarily by the Dockerfile, the build context, the selected base image, and the way the image is executed. Small changes in any of these areas can affect build time, transfer time, startup behavior, vulnerability exposure, and operational troubleshooting.
The most reliable approach is to treat a Dockerfile as a build specification rather than a list of shell commands. Keep the input context deliberate, place stable instructions before frequently changing instructions, separate compilation from runtime files, and make the final process explicit. These practices apply whether an image is built locally, by a CI/CD tool, or as part of a Kubernetes deployment.
A practical production checklist
- Choose a maintained base image that matches the runtime requirements.
- Pin the base image to a deliberate version or digest according to your update policy.
- Create a focused
.dockerignorefile. - Copy dependency manifests before application source when dependency installation is a separate step.
- Use multi-stage builds to keep compilers, package managers, and test artifacts out of the runtime image.
- Run the service as a non-root user unless a documented requirement says otherwise.
- Use exec-form
ENTRYPOINTorCMDso the application receives signals correctly. - Do not place secrets in Dockerfile instructions, build arguments, or image layers.
- Build with BuildKit-enabled workflows where available and inspect the resulting image.
- Scan both the base image and the application dependencies before release.
These are review points, not a substitute for testing. A smaller image can still be incomplete, and a clean scan does not prove that the application is configured safely.
Checklist by scenario
When build times are slow
Start with the build context. A large context makes every build needlessly expensive and may expose files that should never be sent to the builder. Exclude version-control metadata, local dependency directories, test output, logs, environment files, editor settings, and other files that are not required to produce the image.
# .dockerignore
.git
.gitignore
node_modules
vendor
coverage
tmp
*.log
.env
.env.*
Dockerfile*
README.mdNext, order instructions around stable inputs. Dependency manifests usually change less often than application source, so copy and install them before copying the rest of the source tree. The exact commands depend on the language and package manager, but the pattern is reusable:
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run buildWhen a source file changes, this arrangement can preserve the dependency-installation layer. Verify the behavior in your own builder because cache reuse depends on the complete instruction, its inputs, and the builder configuration.
When the image is larger than necessary
Use a multi-stage build. The build stage can contain compilers, development headers, test tools, and package-manager caches. The runtime stage should receive only the executable, compiled assets, configuration defaults, certificates, and shared libraries it actually needs.
FROM golang:1.23-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/service ./cmd/service
FROM debian:bookworm-slim AS runtime
RUN useradd --system --create-home --uid 10001 appuser
WORKDIR /app
COPY --from=build /out/service ./service
USER 10001
ENTRYPOINT ["/app/service"]Do not select a minimal runtime image blindly. If the application needs a shell, certificates, timezone data, a dynamic linker, or native libraries, removing those components can create failures that appear only in production. Confirm the runtime requirements with integration tests.
When security is the primary concern
Use a base image appropriate for the application and establish how it will be updated. A smaller image can reduce the number of installed packages, but size alone is not a security control. Review the image's operating-system packages, language dependencies, permissions, exposed interfaces, and runtime user.
Prefer a non-root user in the final stage. Create directories and assign ownership during the build rather than starting as root and relying on an entrypoint script to change identity later:
RUN useradd --system --uid 10001 appuser \
&& mkdir -p /app/data \
&& chown -R appuser:appuser /app
USER appuserKeep credentials out of the Dockerfile. Build arguments and environment variables can be visible through build history or image metadata. Use the secret-handling mechanism provided by your build system for temporary build credentials, and inject runtime secrets through the deployment platform.
When the container behaves poorly in CI or Kubernetes
Use exec-form process declarations rather than a shell-form command when the application should be the container's main process:
ENTRYPOINT ["/app/service"]
CMD ["--config", "/etc/service/config.yaml"]This makes signal delivery and exit-status handling more predictable. It also helps graceful shutdown work as intended. For Kubernetes workloads, pair correct process handling with suitable startup, readiness, and liveness behavior. See the Kubernetes health checks checklist when deciding which checks belong in a deployment.
Layer-caching reference
| Dockerfile area | Recommended placement | Why it matters |
|---|---|---|
| Base image | At the beginning | Changes only when the runtime or update policy changes. |
| System packages | Before application source | Allows later source changes to reuse the system-package layer. |
| Dependency manifests | Before source code | Preserves dependency-installation cache when source files change. |
| Application source | After dependency setup | Localizes frequent changes to later layers. |
| Build and test commands | After required inputs | Makes the output reflect the current source and dependencies. |
| Runtime copy | In the final stage | Keeps build tools and intermediate files out of production. |
What to double-check
Build context and reproducibility
Run builds from the intended directory and confirm that the Dockerfile receives only required files. Make dependency installation deterministic where the ecosystem supports lockfiles. Avoid commands that silently download unpinned tools during a release build unless that behavior is intentional and controlled.
BuildKit can provide useful features such as improved caching and temporary mounts. For example, a package-manager cache mount can speed repeated builds without copying the cache into the final image:
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txtThe syntax and cache location must match the package manager and builder. Treat cache mounts as a performance aid, not as a source of required application files.
Runtime contents
Inspect the image rather than assuming the final stage contains only the intended files. Useful commands include:
docker image history --no-trunc my-service:review
docker image inspect my-service:review
docker run --rm --entrypoint /bin/sh my-service:review
# If the image has no shell:
docker export $(docker create my-service:review) -o image.tar
tar -tf image.tar | lessCheck the configured user, entrypoint, command, working directory, environment variables, exposed ports, labels, and image size. Avoid publishing an image merely because it builds successfully; start it in a test environment and exercise its health endpoint, shutdown path, filesystem permissions, and required network connections.
Scanning and release review
Use a container scanner that can inspect operating-system packages and application dependencies. Review findings in context, including whether a vulnerable component is present in the final runtime stage, whether a fix is available, and whether the component is reachable or required. Do not suppress findings without recording the reason and an owner.
For release workflows, generate an image with a traceable tag or digest, scan the exact artifact that will be deployed, and retain enough metadata to connect the artifact to its source revision and build. Your CI/CD platform may support these steps directly; otherwise, add them as explicit pipeline stages. The DevOps toolchain checklist can help place image builds, scans, and promotions into a broader delivery workflow.
Common mistakes
- Copying everything early: A broad
COPY . .near the top invalidates later cache layers whenever any local file changes. - Using one stage for everything: Build tools, source code, tests, and package caches can remain in the production image.
- Choosing an image only by size: An aggressively minimal image may omit certificates, native libraries, or debugging utilities that the service needs.
- Running as root by default: This increases the impact of an application compromise and often creates avoidable filesystem ownership problems.
- Using shell-form commands: A shell wrapper can interfere with signal delivery and process exit behavior.
- Adding secrets during build: Removing a secret in a later layer does not necessarily remove it from the image's history.
- Installing packages without cleanup: Package indexes and temporary files increase image size when they are left in the same runtime stage.
- Ignoring the final stage during scanning: Scan the artifact that will run, while also tracking risks inherited from its base image.
- Assuming a Dockerfile is portable: CPU architecture, libc choice, filesystem paths, and native dependencies can differ between build and deployment environments.
When to revisit
Review the Dockerfile whenever the runtime version, base image, package manager, build system, deployment platform, or application startup behavior changes. Revisit it before a major release, after a security advisory affecting the base image or dependencies, and when build times or image transfer times begin to affect delivery.
Set a recurring maintenance task around your team's planning cycle to rebuild from the current approved base image, inspect the dependency lockfiles, run the image scanner, and compare the result with the previous release. Also repeat the review when moving between CI/CD platforms or changing builder settings, since cache behavior and secret handling can differ. Teams evaluating deployment automation can compare these practices with their broader pipeline design in GitHub Actions and GitLab CI workflows.
Final pre-release checklist
- Confirm the build context excludes local files and secrets.
- Verify dependency installation is cache-friendly and reproducible.
- Build the runtime stage separately from compilation and testing tools.
- Inspect the image history, configuration, user, entrypoint, and filesystem.
- Run the image as the intended non-root user and test shutdown behavior.
- Exercise health checks and required application paths in an environment close to production.
- Scan the exact image digest that will be promoted.
- Record exceptions, ownership, and the next review trigger.
A Dockerfile is ready for release when its trade-offs are visible and tested—not simply when it produces a small image. Keep this checklist with the service repository so it can be repeated whenever the inputs, workflows, or tools change.