DevOps · 13 min read
Dockerfile Security Best Practices for Safer Images
By StringTo Editorial Team · Updated
Dockerfile security best practices reduce the attack surface and supply-chain risk of container images before they reach a registry or production runtime. A container is not a security boundary by itself, and a Dockerfile that builds successfully can still embed credentials, run as root, install unnecessary software, use an untrusted base image, or produce layers with known vulnerabilities. A safer workflow combines deliberate Dockerfile design, reproducible dependency handling, automated linting, image scanning, provenance controls, and restrictive runtime settings. This guide explains the checks that matter most, with practical examples and a review checklist you can apply during development and continuous integration.
Choose trusted, minimal, and pinned base images
A secure Dockerfile starts with a base image from a source you trust. Prefer official or internally approved images with documented maintenance, vulnerability response, and update policies. Avoid unfamiliar images selected only because they are small or convenient; every inherited package and configuration becomes part of your application's supply chain.
Smaller images usually contain fewer packages to patch, but minimalism must remain compatible with the application. Distroless and slim variants can reduce utilities available to an attacker, while also making interactive debugging harder. Select the smallest practical runtime base and keep diagnostic tooling outside the production image.
Tags are mutable references. Pinning a digest makes the selected image content reproducible, but it also means security updates are not received automatically. Use an automated process to detect updated trusted digests, review the change, rebuild the image, and rescan it. A stale digest is reproducible but not necessarily secure.
# Pin the reviewed content while retaining the human-readable tag.
FROM node:22-alpine@sha256:REPLACE_WITH_VERIFIED_DIGEST
WORKDIR /app- Use official or organization-approved image sources.
- Choose the smallest base that reliably supports the workload.
- Pin reviewed images by digest for reproducible builds.
- Automate base-image update, rebuild, test, and scan workflows.
Use multi-stage builds to separate build and runtime tools
Multi-stage builds let compilers, package managers, source files, and test utilities remain in a build stage while only required artifacts enter the final image. This reduces image size and limits tools available if the running application is compromised.
Treat every copied artifact as untrusted build output. Copy from an explicitly named stage, use precise paths, and verify that the final image does not contain source maps, test credentials, package-manager caches, private registry configuration, or temporary files. Multi-stage syntax does not prevent accidental copying when broad paths are used.
Build and runtime stages can use different base images, but compatibility must be tested. Native libraries, certificates, timezone data, and dynamically linked binaries may be required at runtime. Add only the dependencies the application actually needs rather than copying an entire build filesystem.
FROM node:22-alpine AS build
WORKDIR /src
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS runtime
COPY --from=build /src/dist /usr/share/nginx/html- Keep compilers and development dependencies out of runtime stages.
- Copy specific artifacts from explicitly named stages.
- Inspect the final image for caches, source files, and build credentials.
- Test runtime dependencies after changing the final base image.
Prevent secrets from entering image layers
Never place passwords, tokens, private keys, or production configuration in ENV or ARG instructions. Build arguments and environment variables can appear in image metadata, build logs, caches, and intermediate layers. Deleting a secret in a later RUN instruction does not remove it from the earlier layer where it was introduced.
Use the build system's secret-mount capability when a private dependency must be accessed during a build. The command should read the temporary secret without copying it into the filesystem that becomes the image. Restrict build logs and cache sharing, and rotate credentials immediately if they have ever been committed or built into an image.
Runtime secrets should come from the deployment platform's secret-management mechanism and be exposed only to workloads that require them. Do not bake environment-specific secrets into one image per environment. Build a single reviewed artifact and inject configuration at deployment time.
# BuildKit example: the token is mounted for this command only.
# syntax=docker/dockerfile:1
FROM node:22-alpine AS build
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# Build with an approved secret source:
# docker build --secret id=npmrc,src=$HOME/.npmrc .- Do not store secrets in ARG, ENV, COPY, or committed files.
- Use ephemeral build-secret mounts for private dependencies.
- Prevent secret values from appearing in logs and shared caches.
- Inject runtime secrets through the deployment platform.
Control the build context with .dockerignore and precise COPY
The build context can include credentials, Git history, local environment files, test fixtures, dependency directories, and unrelated project data. A .dockerignore file reduces the material sent to the builder and makes accidental COPY operations less dangerous. It is a protective layer, not permission to keep secrets inside the project directory.
Prefer explicit COPY instructions over copying the entire context. Copy dependency manifests first when cache behavior benefits from it, then copy only the source and configuration required by the build. Review ownership and permissions when files enter the image, especially executable scripts and private application configuration.
Use ADD only when its special behavior is deliberately required and understood. For ordinary local files, COPY is clearer. Remote downloads should be fetched through a controlled command or build process that verifies integrity and transport, rather than accepted without a checksum or signature.
# .dockerignore
.git
.env*
*.pem
node_modules
coverage
Dockerfile*
README.md
# Dockerfile
COPY --chown=node:node package*.json ./
COPY --chown=node:node src ./src- Exclude VCS data, secrets, local dependencies, and test output.
- Use narrowly scoped COPY paths.
- Set appropriate ownership and file permissions.
- Verify the integrity of remotely acquired build artifacts.
Install dependencies reproducibly and minimize packages
Install only packages required to build or run the application. Extra shells, network clients, editors, and debugging utilities increase the number of components that need patching and can help an attacker after a compromise. Keep temporary build dependencies in an earlier stage or remove them within the same layer in which they are installed.
Use lockfiles and deterministic package-manager commands where the ecosystem supports them. Verify registry configuration and dependency integrity, and avoid unreviewed scripts from packages. Pinning every dependency can improve repeatability, but dependencies still require an automated update process so security fixes are not indefinitely postponed.
Combine package-index refresh, installation, and cache cleanup in a single RUN instruction where appropriate. Never reuse an old package index from a previous layer. The exact commands differ by distribution, so follow the base image's supported package-management practices rather than copying cleanup commands blindly.
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build
# Install production dependencies only when the runtime needs Node packages.
RUN npm prune --omit=dev- Install only necessary system and application packages.
- Use reviewed lockfiles and deterministic install commands.
- Clean package caches in the layer that creates them.
- Automate dependency updates, tests, and vulnerability scans.
Run the application as a non-root user
Processes run as root by default in many container images. If the application is compromised, root inside the container can increase the impact of misconfigured mounts, excessive capabilities, or runtime vulnerabilities. Create or use a dedicated unprivileged user and switch with USER before the final command.
Set ownership during COPY when supported instead of recursively changing large directory trees in a later layer. Ensure the user can read application files and write only to explicitly required locations. Applications needing a low-numbered port can usually listen on a higher internal port and be exposed through the container platform's networking layer.
The Dockerfile user is only one control. At runtime, prevent privilege escalation, drop unnecessary Linux capabilities, use a read-only root filesystem where practical, and avoid privileged containers and unsafe host mounts. Validate that deployment configuration does not override the intended user.
FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
USER node
EXPOSE 8080
CMD ["node", "server.js"]- Set an explicit non-root USER in the final stage.
- Grant write access only to required directories.
- Use high application ports instead of requiring root.
- Reinforce image settings with restrictive runtime controls.
Write safer RUN, CMD, ENTRYPOINT, and health checks
Prefer the JSON exec form for CMD and ENTRYPOINT when the application should receive signals directly and shell expansion is unnecessary. Shell-form commands introduce a shell process and can complicate quoting, signal handling, and argument behavior. If a wrapper script is required, make it minimal, reviewed, and capable of forwarding signals correctly.
Treat untrusted build arguments as data, not executable shell fragments. Quote variables carefully and avoid constructing commands from uncontrolled input. Download commands should fail on HTTP errors, verify integrity, and avoid piping remote content directly into a shell without review.
A HEALTHCHECK can communicate basic container health, but it should be lightweight, deterministic, and supported by the runtime environment. Do not install a large network utility solely for the health check without considering the added attack surface. Orchestrated environments may instead define probes in deployment configuration.
# Exec form avoids an unnecessary shell for the main process.
ENTRYPOINT ["node"]
CMD ["server.js"]
# Use only when the image contains the required check utility.
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD ["node", "healthcheck.js"]- Use exec-form CMD and ENTRYPOINT when shell behavior is unnecessary.
- Do not construct shell commands from untrusted build arguments.
- Verify downloads instead of piping remote scripts directly to a shell.
- Keep health checks small and aligned with runtime orchestration.
Scan, inventory, sign, and continuously rebuild images
Lint the Dockerfile early, then scan the built image because Dockerfile analysis cannot see every inherited package or generated artifact. Define severity thresholds and an exception process that records ownership, justification, compensating controls, and expiration. A scanner result requires context; not every finding is exploitable, and an absence of findings is not proof of safety.
Generate a software bill of materials when supported by the build system and retain provenance that identifies source, builder, dependencies, and build parameters. Sign or attest approved images using the organization's supply-chain tooling, then configure deployment policy to verify the trusted identity and artifact where practical.
Container images do not become permanently safe after release. Monitor base images and dependencies, rebuild when fixes are available, rescan the new artifact, and redeploy it through the normal tested pipeline. Prefer immutable deployment references so the reviewed artifact is the artifact that runs.
# Example CI sequence; choose tools approved by your organization.
docker build --pull -t registry.example.com/api:$COMMIT_SHA .
# lint Dockerfile
# scan the resulting image and SBOM
# sign or attest the reviewed digest
# deploy by immutable digest after policy checks- Lint source and scan the final image.
- Track justified exceptions with owners and expiry dates.
- Generate SBOM and provenance information.
- Sign approved artifacts and continuously rebuild for security updates.
Final secure Dockerfile review checklist
A secure container workflow is layered. The Dockerfile should reduce inherited and installed components, keep credentials outside layers, separate build tools from runtime artifacts, and establish an unprivileged execution model. The build pipeline should add tests, linting, vulnerability scanning, provenance, and controlled publication.
Runtime security remains essential because image controls cannot restrict every deployment choice. Review capabilities, filesystem mutability, mounts, environment variables, service accounts, network access, resource limits, and orchestration policies. A strong Dockerfile can be weakened by an overly privileged Docker Compose or Kubernetes configuration.
Use StringTo's Dockerfile Linter for quick checks of common build and security mistakes, then scan the resulting image with organization-approved tooling. Validate Docker Compose configuration separately when it controls runtime privileges, mounts, networks, and secrets.
Review order:
1. Trust and pin the base image.
2. Minimize build context, packages, and final artifacts.
3. Keep all secrets out of image layers.
4. Set ownership and run as non-root.
5. Use safe commands and reproducible dependencies.
6. Lint, build, test, scan, inventory, and attest.
7. Apply least privilege again at runtime.
8. Rebuild and redeploy when security updates arrive.- Make security checks part of every pull request.
- Review the final image, not only the Dockerfile source.
- Treat deployment configuration as part of container security.
- Keep update and incident-response ownership explicit.
Frequently asked questions
What are the most important Dockerfile security best practices?
Use a trusted minimal base, pin reviewed content, keep secrets out of layers, minimize dependencies, separate build and runtime stages, run as a non-root user, lint the Dockerfile, scan the final image, and apply least privilege at runtime.
Why should a Docker container run as non-root?
An unprivileged user limits what a compromised application can do inside the container and reduces the impact of unsafe mounts, capabilities, and runtime weaknesses. It must be combined with restrictive runtime configuration.
Do multi-stage builds make Docker images secure?
They reduce unnecessary build tools and files in the runtime image, but they do not guarantee security. You must still control copied artifacts, base images, secrets, dependencies, permissions, vulnerabilities, and runtime privileges.
Can Docker build arguments safely store secrets?
No. Build arguments and environment variables can leak through metadata, logs, caches, or layers. Use ephemeral build-secret mounts and inject runtime secrets through an approved secret-management system.
Should Docker base images be pinned by digest?
Digest pinning improves reproducibility and ensures the reviewed content is selected, but pinned images still need an automated update process. Monitor trusted upstream images, review new digests, rebuild, test, and rescan regularly.
Is a Dockerfile linter enough to secure an image?
No. A linter finds source-level patterns, while the final image also contains inherited packages and generated artifacts. Combine linting with image scanning, SBOM review, provenance, runtime controls, testing, and ongoing rebuilds.