In brief
A production Dockerfile is a supply-chain and runtime contract. It should create the same reviewed artifact reliably, contain only required runtime content, expose clear lifecycle behavior, and avoid granting the application unnecessary privilege.
Key takeaways
- Use multi-stage builds to separate compilation dependencies from runtime content.
- Order stable dependency steps before frequently changing source to improve cache reuse.
- Keep secrets out of build arguments, environment metadata, contexts, and image layers.
- Run as a non-root user and test the final image under realistic runtime controls.
Treat the Dockerfile as release engineering code
A Dockerfile does more than copy an application into a base image. It selects part of the software supply chain, defines build reproducibility, controls which tools reach production, establishes the process command and runtime identity, and influences how quickly the application can be patched. Review it with the same care as application and infrastructure code.
Begin with a trusted, maintained base image that fits the runtime. A minimal image can reduce packages and findings, but extreme minimalism can complicate certificates, time zones, native libraries, diagnostics, and incident response. Choose the smallest practical runtime that the team can patch and operate safely, then record how base-image updates are tested and released.
Use multi-stage builds to define a clean runtime boundary
Build dependencies such as compilers, source files, package caches, and development tooling rarely belong in the final image. Multi-stage builds let one stage restore and compile the application while a later stage receives only runtime packages and generated output. Docker's official guidance recommends this separation because it can reduce image size and make the final artifact easier to understand.
Name stages clearly and copy explicit paths from the build stage. Verify that production dependencies remain present and development dependencies do not. Build the intermediate stage during diagnosis when necessary, but scan and deploy the final runtime stage. A smaller result is useful only if it still starts reliably and includes required certificates, libraries, and assets.
Design cache behavior for speed and correctness
Docker reuses unchanged layers, so instruction order matters. Copy dependency manifests and install locked dependencies before copying frequently changing application source. Keep the build context narrow with `.dockerignore` so local dependencies, Git history, logs, test output, environment files, and unrelated artifacts do not invalidate cache entries or enter the builder unnecessarily.
Use cache mounts for package-manager data when supported and useful. Do not solve non-deterministic builds by permanently disabling the cache. If a no-cache comparison changes the output, identify whether an instruction depends on mutable downloads, missing lock data, timestamps, or external state. Reproducibility is more valuable than an accidentally fresh build.
- Install from a committed lockfile and fail when it disagrees with the manifest.
- Combine package index refresh and installation in the same layer where appropriate.
- Remove temporary package data in the layer that creates it.
- Use explicit build inputs rather than downloading unversioned content at runtime.
Keep credentials out of every image layer
Build arguments and environment instructions are not secret stores. Their values can be exposed through metadata, history, logs, caches, attestations, or CI output. Copying a credential and deleting it in a later instruction can leave it recoverable from an earlier layer. Exclude local secret files from the build context and use an approved BuildKit secret or SSH mount when a private dependency is required.
Supply runtime secrets through the deployment platform's secret mechanism and grant access through workload identity where possible. Separate configuration from the image so the same tested digest can move through environments. Inspect final metadata and build logs for accidental exposure before publishing to a registry.
Define a least-privilege runtime
Create or select a non-root runtime user and ensure copied files have intentional ownership. Set a stable working directory, use exec-form entrypoint and command values so the main process receives termination signals, and document the listening port. Write only to explicit mounted paths or temporary filesystems so a read-only root filesystem can be used when the application supports it.
A Dockerfile cannot enforce every deployment control. Test the image with dropped capabilities, `no-new-privileges`, resource limits, read-only filesystems, controlled mounts, and the target platform's security profile. Add only the exception the workload proves it needs; privileged mode and Docker socket access should never be routine application requirements.
Make health and shutdown behavior observable
A health check should be fast, bounded, side-effect free, and aligned with the signal the operator needs. Confirm the final image contains the command used by the check, give slow-starting applications a realistic start period, and avoid making a liveness-style signal fail whenever an optional downstream system is briefly unavailable.
The main process should receive termination signals, stop accepting new work, finish or safely abandon in-flight work, close dependencies, and exit within the platform's grace period. Test `docker stop` behavior in CI or pre-production. A process that requires a force kill can interrupt requests and corrupt work during normal deployments.
Publish an immutable, tested artifact
Build the image once, run application and security tests against that exact image, push it with an intentional release tag, record its content digest, and promote the same digest through environments. Human-readable tags are useful, but tags can move; the digest proves which content was deployed. Avoid relying on `latest` for controlled production releases.
Rebuild regularly when the base image or application dependencies receive fixes. Scanning is a decision input rather than a one-time gate: investigate severity, exploitability, exposure, reachability, and available remediation. After deployment, verify application transactions, health, logs, resource use, and graceful rollback—not only that the registry accepted the image.