A production-ready Dockerfile is more than a set of commands that builds successfully. It should produce a predictable, appropriately sized image, avoid exposing credentials, run with limited privileges, and provide enough health and runtime information for your deployment platform to operate it safely. This Dockerfile best practices checklist gives you a repeatable review workflow for application images, whether you deploy to a managed platform, virtual machine, or cloud container service.
Overview
Improving a Dockerfile for production starts with separating build concerns from runtime concerns. Developers need compilers, package managers, test tools, and source files during a build; the running application usually needs only its compiled output, production dependencies, configuration, and a minimal set of operating-system files.
Review the Dockerfile in this order:
- Build context: Confirm that unnecessary files are excluded with a suitable
.dockerignore. - Base image: Choose a maintained image that matches the application runtime and operational requirements.
- Build stages: Use multi-stage builds when compilation or asset generation is not required at runtime.
- Layer design: Copy dependency manifests before application source when that improves build-cache reuse.
- Security: Keep secrets out of image layers and run the application as a non-root user where practical.
- Runtime behavior: Define a clear command, configuration boundary, logging approach, and health-check strategy.
- Verification: Inspect image size, scan for vulnerabilities, test the image as it will run, and document exceptions.
A smaller image is not automatically a safer image, and a successful build is not proof that the container is production-ready. Treat optimization, security, and operability as separate review areas.
Checklist by scenario
For every production Dockerfile
- Use a deliberately selected base image rather than an unexplained default.
- Record the runtime or base-image version in a way that supports repeatable builds. A moving tag may be convenient, but it can change the result of a later build.
- Add a
.dockerignorefile that excludes source-control metadata, local dependencies, build output, logs, environment files, editor settings, and test artifacts that are not needed in the image. - Keep application configuration in environment variables or a platform configuration system instead of baking deployment-specific values into the image.
- Make the container's main process explicit with
CMDorENTRYPOINT. - Ensure the application writes logs to standard output and standard error unless there is a deliberate, documented alternative.
- Run a vulnerability scan and investigate findings rather than treating the scan as a pass-or-fail substitute for review.
When building a compiled application or front-end bundle
Use a multi-stage Docker build so the compiler and development dependencies remain in a builder stage. The final stage should copy only the files required to serve or run the application.
# syntax=docker/dockerfile:1
FROM node:22-bookworm AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:stable
COPY --from=build /app/dist /usr/share/nginx/html
The exact runtime image depends on the application. A static front-end may need only a web server, while a server-rendered application may need a language runtime and production-only packages. Check the output path, ownership, startup command, and web-server configuration before assuming that a builder-to-runtime copy is sufficient.
When deploying a server application
- Install production dependencies separately from development dependencies where the package manager supports it.
- Copy lockfiles and manifest files before source code to make dependency layers easier to reuse.
- Use a multi-stage build if native modules, compilers, or test tooling are needed only during installation or compilation.
- Set a working directory and use an explicit, non-root runtime user.
- Confirm that the application listens on the expected interface and receives its port through configuration when required by the hosting platform.
- Handle termination signals correctly so the process can shut down cleanly during deployment or scaling operations.
FROM python:3.12-slim AS runtime
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
COPY --from=build /opt/venv /opt/venv
COPY app ./app
RUN useradd --create-home --uid 10001 appuser \
&& chown -R appuser:appuser /app
USER appuser
CMD ["/opt/venv/bin/gunicorn", "app.wsgi:application"]
This example assumes a separate build stage created the virtual environment. Adapt the command and paths to the framework rather than copying it unchanged.
When reducing image size
- Measure the current image before changing it.
- Inspect which layers contain the largest files.
- Remove build tools from the final stage through multi-stage builds rather than deleting them in a later layer.
- Install only the packages the running process needs.
- Combine related package-manager operations where appropriate and clean package metadata according to the base image's conventions.
- Do not remove diagnostic tools that are required for safe operation without providing an alternative debugging method.
Layer deletion does not necessarily reduce the stored image when the unwanted files were introduced in an earlier layer. Preventing unnecessary files from entering the final stage is usually clearer and more effective.
What to double-check
Base images and pinning
Choose an image with a clear maintenance process, a runtime version compatible with the application, and a package set appropriate for the workload. Decide how much reproducibility you need. A version tag is readable and easy to update; a digest can make the exact source immutable, but it adds an update task. Whichever approach you use, record how the image is reviewed and refreshed.
Check the image's operating-system family, architecture support, package availability, and documented support window before committing to it. Avoid choosing a minimal variant solely because it has fewer packages; compatibility and debuggability also matter.
Dependency caching and layer order
Place infrequently changing inputs before frequently changing inputs. For example, copy a package manifest and lockfile, install dependencies, then copy application source. A source-code edit can then avoid repeating dependency installation when the relevant build cache remains available.
Use BuildKit features, such as cache mounts or secret mounts, when they are supported by your build environment and understood by your team. A cache can improve build speed, but it should not be required for correctness. A secret mount should prevent credentials from being written into an image layer; verify the resulting image rather than assuming the syntax alone guarantees safe handling.
Secrets and sensitive files
Never place API keys, private certificates, passwords, or production environment files in COPY instructions, build arguments, or committed Dockerfiles. Build arguments can be visible in build history or metadata depending on the tooling and should not be treated as a secure secret store. Supply sensitive values through the CI system or deployment platform using its documented secret mechanism.
After a build, inspect image history, exported layers, and the final filesystem for accidental credentials. If a secret has entered an image or repository, remove the exposure and rotate the credential; deleting the line in a later commit is not enough.
Users, permissions, and health
Use a dedicated non-root user for the application when the runtime and hosting environment allow it. Make only the required directories writable, and verify that the application can read its files and write to any intended temporary or cache location.
A health check should test a meaningful condition, such as the application responding on its internal health endpoint. It should be lightweight, have reasonable timing, and distinguish application readiness from simple process existence when the deployment platform supports separate readiness and liveness concepts. Do not add a health check that depends on a tool absent from the final image.
Common mistakes
| Mistake | Likely effect | Better review question |
|---|---|---|
| Using one large image for both build and runtime | More packages, larger transfers, and a wider attack surface | Can the runtime stage contain only compiled output and production dependencies? |
| Copying the entire repository before installing dependencies | Frequent source changes invalidate dependency-cache layers | Can manifests and lockfiles be copied first? |
| Running as root by default | A process compromise may have broader consequences inside the container | Can a dedicated user run the application without elevated privileges? |
| Embedding secrets in the Dockerfile | Credentials may persist in history, layers, logs, or source control | Can the value be provided only at build or runtime through a secret mechanism? |
| Using a floating base-image tag without an update process | Build results can change unexpectedly or updates can be missed | Who reviews base-image changes and how are they tested? |
| Adding a health check that tests the wrong thing | Healthy processes may be marked unavailable, or broken applications may appear ready | Does the check represent the service's actual readiness? |
| Optimizing size without testing behavior | Required certificates, shells, libraries, or diagnostic capabilities may be removed | Does the final image pass startup, request, shutdown, and monitoring tests? |
Another frequent mistake is assuming that Dockerfile security can be solved entirely inside the file. The build pipeline, registry permissions, host configuration, runtime privileges, network policy, dependency process, and secret-management system all affect the final risk. Review the image as one part of the deployment workflow. For broader deployment planning, see the practical guide to hosting, CI/CD, domains, and rollbacks.
When to revisit
Run this checklist before a production release, after a major dependency or language-runtime upgrade, and whenever the CI/CD pipeline or hosting platform changes. It is also worth revisiting after a vulnerability report, a change in application architecture, a move between CPU architectures, or a decision to add background workers, scheduled jobs, or additional service ports.
For a repeatable review, save the following with the project:
- the Dockerfile and
.dockerignorereview date; - the selected base-image reference and update owner;
- the built image size and the largest layers;
- scanner findings, accepted risks, and remediation dates;
- the user, exposed ports, startup command, and health-check behavior;
- test results for startup, normal requests, graceful shutdown, and configuration errors.
Before approving the next image, build it from a clean environment, scan the resulting artifact, run it with production-like configuration, and confirm that no credentials or unnecessary source files are present. Then compare the result with the previous image. This simple before-and-after record makes image-size growth, new vulnerabilities, and behavior changes easier to detect during routine deployment work.
Dockerfiles should be updated when their inputs change, not only when a release is blocked. Pair this checklist with your deployment and performance reviews, including the comparison of production hosting options and the web performance optimization checklist, so the container remains aligned with the way the application is actually operated.