A better Dockerfile can make builds faster to repeat, images easier to inspect, and deployments safer to maintain. This practical checklist explains how to choose a base image, organize layers, use multi-stage builds, protect secrets, run as a non-root user, and review the result before shipping.
Overview
A Dockerfile is both a build recipe and a record of how an application becomes a runnable image. Small decisions in that recipe affect build time, image size, security review, debugging, and reproducibility. The goal is not to produce the shortest file or the smallest possible image at any cost. The goal is a predictable image containing what the application needs—and little else.
Start by separating four concerns:
- Build inputs: source code, dependency manifests, compilers, package managers, and test tools.
- Runtime inputs: the application, its production dependencies, certificates, configuration defaults, and the runtime itself.
- Build behavior: layer order, cache usage, pinned inputs, and the build context.
- Operational behavior: the user, exposed port, health check, startup command, and signal handling.
Keeping these concerns explicit makes an image easier to change when a dependency is upgraded, a vulnerability is investigated, or a service moves from local development to deployment. For broader environment planning, pair this checklist with the local development environment setup guide.
Checklist by scenario
1. Choosing a base image
- Select a trusted image that includes the runtime your application actually requires.
- Prefer a clearly identified, supported image variant rather than an unqualified floating choice.
- Use a minimal runtime image when it remains compatible with the application and its native dependencies.
- Check whether your application needs shell utilities, system libraries, certificate bundles, or a particular C library.
- Record the reason for the chosen image in project documentation, especially when selecting between a full, slim, or minimal variant.
“Smallest” is not automatically “best.” A very minimal image may complicate diagnostics or fail when a native dependency expects a library that is absent. Choose the smallest practical base, then verify it with the application’s tests and startup checks.
2. Ordering instructions for useful caching
Place stable instructions before frequently changing files. A common pattern is to copy dependency manifests first, install dependencies, and only then copy the application source:
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
The exact runtime version and package manager should match the project; the important principle is the order. If source files change but dependency manifests do not, the dependency-installation layer may remain reusable. Do not treat cache reuse as a substitute for correctness: dependency installation should still be deterministic and should not silently depend on files outside the declared inputs.
3. Using a multi-stage Docker build
A multi-stage Docker build keeps build-only tools out of the final image. Compile, bundle, or test in one stage, then copy only the artifacts needed at runtime into a separate runtime stage.
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
Adapt the example to your framework and package manager. Confirm where the build output is written, whether runtime dependencies are copied or installed separately, and whether native modules need to be built in an environment compatible with the runtime stage. Multi-stage builds are especially useful for compiled services, frontend bundles, and applications whose test or build toolchain is much larger than their production requirements.
4. Cleaning the build context
Create a .dockerignore file so Docker does not send unnecessary files as build input. Common entries include dependency directories, version-control metadata, local environment files, logs, coverage output, editor settings, and generated artifacts that are rebuilt inside the image.
node_modules
.git
.env*
coverage
*.log
dist
Review each exclusion against the build. If a generated file is required and is not recreated during the build, excluding it will create a confusing failure. Never assume a local ignore pattern is harmless without testing a clean build.
5. Running as a non-root user
Use a dedicated application user when the process does not need elevated privileges. Create or select that user in the runtime stage, make sure the application directory is readable and writable where necessary, and switch users before the startup command.
RUN groupadd --system app && useradd --system --gid app app
COPY --chown=app:app --from=build /app/dist ./dist
USER app
CMD ["node", "dist/server.js"]
Commands differ across base-image families, so verify the user-management tools available in the selected image. Also test temporary-file creation, cache directories, uploaded files, and log paths under the non-root account.
6. Handling secrets and configuration
Do not place passwords, tokens, private keys, or local environment files in a Dockerfile or copied build context. Values passed through build arguments or environment instructions can become visible through image metadata or layers. Use the secret-management mechanism provided by your build and deployment workflow, and inject runtime configuration at runtime rather than baking environment-specific values into the image.
For token-related troubleshooting, the JWT decoder guide can help inspect claims safely; do not paste sensitive production tokens into tools or logs.
What to double-check
| Area | Review question | Useful verification |
|---|---|---|
| Reproducibility | Would another developer or CI runner receive the same intended inputs? | Use lockfiles, a controlled context, and an explicit build command. |
| Runtime contents | Does the final stage contain only what the service needs? | Inspect the image and compare it with the build-stage dependencies. |
| Security | Are secrets excluded, permissions limited, and the process non-root where practical? | Review Dockerfile instructions, image metadata, and runtime behavior. |
| Startup | Does the command handle signals and exit with a meaningful status? | Run, stop, and restart the container in a disposable environment. |
| Health | Can the service’s readiness or liveness be checked without masking failures? | Use an appropriate health check or deployment-level probe. |
| Networking | Does the application listen on the expected interface and port? | Test from inside and outside the container using the actual deployment configuration. |
An EXPOSE instruction documents an intended port; it does not publish that port by itself. Likewise, a health check should test a meaningful application condition rather than merely confirming that a process exists. Keep operational assumptions visible in the Dockerfile and deployment configuration.
Common mistakes
- Copying everything too early: Copying the entire repository before installing dependencies can invalidate the cache for every source change.
- Using one large stage for everything: Build tools, test fixtures, and development dependencies can unnecessarily increase the runtime image.
- Using floating inputs without a review process: An unqualified base image or uncontrolled dependency update can change a build unexpectedly. Choose an update policy that fits the project and review changes deliberately.
- Assuming local success proves container correctness: The container may have different filesystem permissions, environment variables, working directories, or available system libraries.
- Running as root by default: This can make file-permission problems invisible during development and increases the impact of an application-level compromise.
- Embedding secrets in convenience instructions: A secret should not appear in the Dockerfile, build context, command history, image labels, or diagnostic output.
- Adding tools for debugging permanently: Temporary troubleshooting utilities belong in a debug workflow or separate diagnostic image unless the service genuinely needs them.
- Ignoring the application’s shutdown behavior: A wrapper command may prevent signals from reaching the real process. Test graceful termination instead of assuming the default command is sufficient.
When a container behaves differently from local development, isolate the difference methodically: inspect environment values, verify the working directory, check file ownership, confirm listening interfaces, and review the final image contents. The frontend debugging checklist and JavaScript debugging workflow offer similar structured approaches for application-level issues.
When to revisit
Review a Dockerfile whenever its inputs or operating assumptions change—not only when a deployment fails. Add a short checklist review to dependency upgrades, runtime-version changes, CI workflow changes, and production release preparation.
- Build from a clean context and confirm that the image still contains the required artifacts.
- Run unit, integration, and startup checks in the built image rather than only on the host.
- Inspect the final image for unexpected build tools, local files, credentials, or oversized directories.
- Test the configured non-root user, health behavior, network binding, and shutdown path.
- Record intentional exceptions, such as a required system library or diagnostic utility, so the next review has context.
Revisit the file after changing the base image family, package manager, build system, application framework, or deployment platform. Also review it when a vulnerability scan, image-size investigation, slow CI build, or recurring environment mismatch reveals that the original assumptions no longer hold. A Dockerfile is production code: keep it readable, testable, and aligned with the service it builds.
For a wider release checklist, see the API development checklist, and use the developer tools guide when formatting or inspecting configuration during the review.