A secure, efficient Dockerfile is the result of several small decisions: selecting an appropriate base image, structuring layers for useful cache reuse, limiting installed software, and checking the final image before deployment. This checklist provides a repeatable workflow for reviewing Dockerfiles used in local development, CI/CD, and cloud-native services.
Overview
Docker image size matters, but it is only one part of a good container build. A smaller image can reduce transfer and startup work, while a carefully constructed image can also reduce unnecessary packages, limit the available attack surface, and make troubleshooting more predictable. The best result depends on the application, runtime, operating system compatibility, and operational requirements.
Use this Dockerfile best practices checklist in order:
- Define what the container must run and what it does not need.
- Select a maintained base image that matches the application runtime.
- Separate build-time dependencies from runtime files.
- Order instructions so stable dependencies can be reused by the Docker build cache.
- Pin application and system dependencies according to your team’s update policy.
- Run the process as a non-root user whenever the application supports it.
- Scan, test, and inspect the resulting image rather than relying only on the Dockerfile text.
A Dockerfile is part of a larger cloud-native developer workflow. The build context, dependency lockfiles, CI configuration, registry settings, runtime permissions, and deployment manifest all affect the security and efficiency of the final container.
Checklist by scenario
1. Choosing a base image
- Use a trusted image source appropriate for the language runtime or operating system.
- Prefer a minimal variant when it provides the libraries and debugging capability the service actually requires.
- Record the selected version and review its release and support status as part of normal maintenance.
- Use a digest when your supply-chain process requires an exact, immutable image reference.
- Test the application against the chosen image rather than assuming that two distribution variants are interchangeable.
There is no universal “smallest is best” choice. A minimal runtime image may reduce unnecessary software, but it can also omit shells, diagnostic commands, certificates, or native libraries that your application needs. A fuller image may be easier to troubleshoot, especially during early development. Decide deliberately, then use a separate production image if the development image contains tools that are not needed at runtime.
2. Separating build and runtime stages
Multi-stage builds allow compilers, package managers, test tools, and source files to remain in a build stage while the runtime stage receives only the files needed to start the service. A generic pattern looks like this:
FROM <runtime-with-build-tools> AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN <install-production-and-build-dependencies>
COPY . .
RUN <run-tests-and-build-application>
FROM <minimal-runtime> AS runtime
WORKDIR /app
COPY --from=build /src/<built-output> ./
USER app
CMD ["<start-command>"]
Replace the placeholders with commands for the application stack. Keep tests in the build pipeline or build stage, and make the runtime stage explicit. Confirm that the copied output includes required configuration defaults, static assets, certificates, and native libraries.
3. Improving Docker build cache reuse
- Copy dependency manifests before copying the full source tree.
- Install dependencies in a dedicated layer so source-only changes do not always invalidate the installation layer.
- Group related commands when doing so avoids leaving temporary package metadata behind.
- Use a carefully maintained
.dockerignorefile to exclude version-control metadata, local dependencies, build output, logs, secrets, and unrelated files. - Use the build features supported by your CI environment for dependency caching, while ensuring that cached data cannot bypass required validation.
Cache optimization should not override correctness. If a dependency version, lockfile, operating-system package, or build argument changes, the affected layer must be invalidated. A fast build that silently uses stale inputs is not an improvement.
4. Running as a non-root user
Create or select a dedicated application user in the runtime stage, assign ownership only where the process needs to write, and switch to that user before the default command runs. Avoid giving the application broad write access to the entire filesystem. If the service needs a writable directory for temporary files, create that directory explicitly and document the requirement.
Also check the deployment configuration. Container-level user settings, mounted volumes, filesystem permissions, and platform security controls can change the effective behavior of the image. Test the container with the same restrictions expected in production.
5. Pinning application and system dependencies
Use lockfiles for language dependencies where the ecosystem supports them, and make sure the lockfile is copied and honored during the build. For operating-system packages, follow the package manager’s supported approach for selecting repeatable versions. Pinning does not eliminate maintenance; it makes updates intentional and reviewable.
Keep the update process practical: review base-image updates, dependency updates, and security findings on a defined schedule, then rebuild and test rather than changing versions manually inside a running container.
Image-size and security trade-offs
| Choice | Potential benefit | Trade-off to review |
|---|---|---|
| Minimal runtime image | Fewer packages and less unused content | Fewer debugging tools and possible compatibility work |
| Full distribution image | Broad compatibility and familiar troubleshooting tools | More installed software to maintain and assess |
| Multi-stage build | Build tools and source can stay out of the runtime image | More stages and explicit artifact selection to maintain |
| Floating version tag | Simple configuration and easy access to new image changes | Builds may change without a corresponding Dockerfile edit |
| Digest-pinned image | Reproducible reference to a specific image artifact | Requires a deliberate update workflow |
| Non-root runtime | Limits the process’s default privileges | File ownership and write-path assumptions must be tested |
What to double-check
Build context and secrets
Inspect the build context before sending it to the Docker daemon or CI builder. A .dockerignore file should exclude local environment files, private keys, cloud credentials, dependency directories, test output, and other material that does not belong in the image. Do not treat a build argument or ordinary environment variable as a secure place for a secret; values can be exposed through build history, logs, metadata, or later image inspection.
Runtime configuration
Keep environment-specific settings outside the image when possible. The image should contain the application and its required runtime files, while deployment configuration supplies environment names, service endpoints, credentials, and resource settings. Verify that the application handles missing or malformed configuration without logging sensitive values.
Health, signals, and shutdown
Make sure the container’s main process receives termination signals correctly and exits within the time allowed by the deployment platform. If a wrapper script is necessary, it should forward signals and return meaningful exit codes. Define health checks only when they represent useful application readiness or liveness behavior; a check that always succeeds does not improve operations.
Image validation
- Build from a clean checkout in CI.
- Run unit, integration, and startup tests appropriate to the service.
- Inspect image layers and confirm that source files, credentials, caches, and build tools were not copied unintentionally.
- Run an image scanner selected by your organization and triage findings by applicability, severity, and available remediation.
- Test the final image, not only the build stage, with the intended user and restricted runtime settings.
- Record the image reference and build metadata needed to trace a deployment back to its source.
Common mistakes
- Copying everything too early: A broad
COPY . .before dependency installation invalidates the cache whenever any source file changes. - Using the build image in production: Compilers, package managers, test data, and source code increase maintenance and review scope when they are not needed at runtime.
- Leaving the process as root: This often happens because ownership was not planned for application directories or mounted volumes.
- Assuming a smaller image is automatically safer: Size is a useful signal, but dependency versions, configuration, permissions, provenance, and runtime behavior also require review.
- Ignoring native dependencies: A runtime image can build successfully yet fail at startup if required shared libraries, certificates, or fonts were excluded.
- Relying only on the latest tag: Moving tags may be convenient for experiments, but production workflows should define how image changes are reviewed and promoted.
- Installing unneeded tools for convenience: Add debugging utilities only when their operational value justifies their maintenance and security implications.
- Scanning only the source repository: Vulnerabilities and accidental content can enter through the base image or generated artifacts, so inspect the final image as well.
When to revisit
Review the Dockerfile whenever one of its inputs or its operating context changes. At minimum, revisit it during these events:
- the base image or operating-system release changes;
- the language runtime, framework, or dependency lockfile is upgraded;
- the build system, cache strategy, or CI runner changes;
- the service adds a native library, background worker, scheduled task, or new writable path;
- a scanner reports a vulnerability or an image is unexpectedly large;
- the deployment platform changes its user, filesystem, networking, or health-check behavior;
- a new service is created from an existing Dockerfile template.
For a practical review, open the Dockerfile and complete this short release gate:
- Confirm the base image, version policy, and compatibility assumptions.
- Check that the build context excludes secrets and irrelevant files.
- Verify that dependency installation uses the intended lockfiles and cache boundaries.
- Confirm that the runtime stage contains only required artifacts and libraries.
- Run the container as its intended non-root user and test startup, shutdown, health, and write paths.
- Build from a clean checkout, scan the final image, and record any accepted exceptions.
Save this checklist with the service’s repository or deployment review process. Repeating the same checks after dependency upgrades and workflow changes turns Docker image optimization and container security into routine cloud DevOps work rather than a last-minute deployment task.