Dockerfile Best Practices: Build Faster, Smaller, and More Secure Container Images
A well-structured Dockerfile makes container builds easier to cache, review, scan, and operate. This practical checklist explains how to control the build context, order layers, use multi-stage builds, select and pin base images, run as a non-root user, configure applications safely, and review an image before it reaches Kubernetes or another production platform.
Overview
A Dockerfile is both a build recipe and part of your application’s operational contract. Every instruction can affect build time, image size, reproducibility, runtime permissions, and the information exposed to a container scanner. The best Dockerfile is not necessarily the shortest one; it is predictable, understandable, and appropriate for the way the image will be built and run.
Start by separating build concerns from runtime concerns. Compilers, package managers, test fixtures, and source files may be necessary to create an application but unnecessary to run it. Multi-stage builds let you keep those tools in an intermediate stage while copying only the required runtime artifacts into the final image.
Use a deliberate build context. A broad context can send local caches, documentation, credentials, test output, and other irrelevant files to the builder. Create a focused .dockerignore file and review it whenever the repository layout changes. Excluding a file from the build context is useful, but it is not a substitute for keeping secrets out of source control and build inputs.
BuildKit-compatible workflows can improve caching and support modern build features, but cache behavior still depends on Dockerfile order and the files referenced by each instruction. Treat the cache as an optimization, not as a correctness mechanism: a clean build should remain successful.
Checklist by scenario
For a basic application image
- Choose a base image that supports the application runtime and has a maintenance process your team can follow.
- Set a working directory with
WORKDIRrather than relying on the image’s default directory. - Copy dependency manifests before application source when the dependency installation step can be cached independently.
- Install dependencies in a reproducible way, using the project’s lockfile where one exists.
- Copy only the files needed to build or run the application.
- Define a clear startup command with
CMDorENTRYPOINT, and verify its signal-handling behavior.
For a compiled application or frontend build
- Use a builder stage containing compilers, development headers, package managers, and test tooling.
- Run tests or validation in the build pipeline before producing the final runtime image.
- Copy the compiled binary or generated static assets into a separate, minimal runtime stage.
- Confirm that native libraries and certificates required at runtime are present in the final stage.
- Do not copy the entire builder filesystem into the production image merely to avoid identifying runtime dependencies.
# Before: build tools and source remain in the runtime image
FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]
# After: dependency and build concerns are separated
FROM node:22 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
USER node
CMD ["node", "dist/server.js"]The versions in this example are illustrative, not a recommendation for every project. Select a runtime and release line that your application supports, then record how the team will update it.
For a security-focused image
- Run the process as a dedicated non-root user whenever the application permits it.
- Keep credentials, tokens, private keys, and environment-specific configuration outside the image.
- Use a package installation step that removes temporary package indexes and build-only files when appropriate for the base distribution.
- Scan the resulting image, but investigate findings rather than treating a scanner’s count as the sole quality measure.
- Pin base-image references and important dependencies according to your team’s update policy. A digest can improve reproducibility, while a readable version tag can make maintenance easier; choose a process that provides both traceability and timely updates.
For a Kubernetes deployment
- Keep configuration in deployment configuration or a suitable secrets system, not in
ENVinstructions for sensitive values. - Make the image’s exposed port and actual listening port agree with the deployment configuration.
- Ensure the application handles termination signals and can shut down without corrupting work.
- Define health behavior at the orchestration layer where appropriate, rather than assuming a Dockerfile instruction alone provides readiness.
- Use an immutable image reference in the release workflow when you need a deployment to identify one exact artifact.
For the next stage of review, compare these image concerns with the broader Kubernetes deployment checklist and choose a rollout method that matches your risk profile using the Kubernetes deployment strategies guide.
Base-image comparison
Base-image selection is a trade-off among compatibility, size, available debugging tools, update practices, and operational familiarity. Test the actual application rather than choosing solely by image label.
| Base-image approach | Useful when | Double-check |
|---|---|---|
| Full distribution image | The application needs broad system compatibility or common troubleshooting tools. | Whether unused packages increase the maintenance and scanning surface. |
| Minimal distribution image | The application has well-understood runtime dependencies and the team values a smaller footprint. | Certificates, native libraries, shell assumptions, and debugging procedures. |
| Distroless-style runtime | The application can run without a shell or package manager in the final image. | How operators will inspect failures and whether the runtime includes required users, certificates, and time-zone data. |
| Specialized or minimal runtime | The language or platform provides a supported image tailored to the workload. | Release support, compatibility with extensions, and the team’s ability to respond to vulnerabilities. |
“Smaller” is not automatically “better.” A slightly larger image may be the more maintainable choice if it has a clear update path and avoids fragile workarounds. Record the reason for the choice so a future maintainer can reassess it.
What to double-check
- Layer ordering: Put stable instructions before frequently changing instructions. Dependency manifests usually change less often than application source, so install dependencies before copying the full source tree when the package manager supports that pattern.
- Cache invalidation: Understand which
COPY,ADD, and build-argument changes invalidate later steps. Never rely on stale cache data for security updates or dependency installation. - Package cleanup: If a package manager creates indexes or temporary archives, remove them in the same layer where they are created when that reduces the final layer size.
- Runtime user: Confirm file ownership and writable directories after switching away from root. A non-root setting that causes the application to fail at startup is not a complete security improvement.
- Secrets: Review build arguments, copied files, logs, and generated artifacts. Values passed during a build can appear in history or intermediate layers depending on the workflow; use an appropriate secret mechanism instead.
- Entrypoint behavior: Test stop, restart, and failure scenarios. Shell wrappers can interfere with signal delivery if they are not designed to forward signals correctly.
- Scanning and provenance: Scan the final image and, where required by your delivery process, retain enough metadata to identify its source, dependency inputs, and build revision.
For pipeline-specific improvements, see How to Speed Up CI/CD Pipelines. For credentials used by builds or deployments, review Secrets Management Tools for DevOps.
Common mistakes
- Using
COPY . .too early: This can invalidate expensive dependency layers whenever any unrelated file changes. Copy manifests first where practical. - Using a floating base reference without an update process: Builds may change unexpectedly, while security fixes may be missed if nobody owns updates. Define how versions or digests are reviewed.
- Installing development dependencies in production: This increases image size and may expose unnecessary tools. Separate build and runtime stages or use the package manager’s production mode.
- Running everything as root: It may hide file-permission assumptions during testing. Create or select a suitable runtime user and test the image with that identity.
- Embedding configuration in the image: Environment-specific values make images harder to reuse and can expose secrets. Keep configuration at deployment time.
- Optimizing before measuring: Removing useful tools or switching base images without testing can create operational friction. Compare build duration, image size, startup behavior, scan findings, and incident-debugging needs.
- Assuming a clean scan means a secure image: Scanners are valuable inputs, but secure operation also depends on permissions, dependency updates, deployment configuration, access controls, and runtime monitoring.
When to revisit
Review a Dockerfile whenever the base image, language runtime, package manager, major dependency set, or build platform changes. Revisit it after a container scan identifies a recurring vulnerability, after a production incident involving startup or shutdown behavior, and before a significant seasonal planning or capacity cycle. A change in CI/CD tooling, BuildKit configuration, registry policy, or Kubernetes runtime assumptions is also a reason to repeat the review.
Use this repeatable workflow:
- Build from a clean context and record the image size, build duration, and resulting image reference.
- Inspect the dependency and base-image inputs, including their update and ownership process.
- Run tests, application startup checks, and a container scan against the final image.
- Test the image as its intended runtime user and verify writable paths, health behavior, and termination handling.
- Compare the result with the previous image and document any deliberate trade-offs.
- Promote the exact tested artifact rather than rebuilding an untracked variant during deployment.
A short Dockerfile review at each meaningful change is usually more effective than a large cleanup performed only after a release problem. Keep the checklist with the repository, make ownership clear, and update the review whenever your application or platform assumptions change.