Dockerfile best practices that reduce your container attack surface

Rotem Natan
Rotem Natan
Jun 28, 2026 | 11 Minutes
Dockerfile best practices that reduce your container attack surface

Key takeaways

  • Your Dockerfile is a security document, not just a build script. Every instruction you write either adds to or subtracts from the attack surface of every container that image ever runs.
  • The highest-impact decisions happen at the top of the file. Base image choice and what you include during the build stage determine the vast majority of your eventual CVE exposure - application code is rarely where the risk originates.
  • Dockerfile security degrades over time without active maintenance. Pinned versions become stale, base images accumulate new CVEs, and what was clean at build time will not stay clean without ongoing scanning and patching.

Containers are often described as isolated and ephemeral, which can create a false sense of security. The isolation is real, but it only contains what is already inside - and what is inside is entirely determined by what was put there at build time. A poorly written Dockerfile does not just produce a large image; it produces an image carrying vulnerabilities, unnecessary privileges, and capabilities that attackers can exploit if they find a way in.

Dockerfile best practices are not a stylistic concern. They are the primary lever teams have for controlling how exposed a container will be before it ever runs. This guide covers the decisions that move the needle most, the patterns that quietly introduce risk, and how to know whether your approach is actually working.

The concept of attack surface and why it matters in container security

Attack surface is the total number of potential entry points through which something can go wrong. In a container context, every package installed, every capability granted, every user with elevated permissions, and every port exposed adds to that surface. The larger the surface, the more opportunities an attacker has - and the more your runtime security controls have to compensate.

The important insight is that attack surface is cumulative. A container running as root with a full-featured OS base image, unnecessary debugging tools, and no read-only filesystem has a dramatically larger surface than a minimal container running as a non-root user with only what the application needs. Neither may have a known CVE on day one - but the first will accumulate far more risk over time, and will be far more dangerous if a vulnerability is ever discovered.

Most of that surface is determined in the Dockerfile. Runtime controls - seccomp profiles, network policies, pod security standards - can restrict what a container can do, but they cannot remove packages or capabilities that were baked in at the image level. Prevention at the build stage is always cheaper and more reliable than compensation at runtime.

How Dockerfile choices shape a container's security before it runs

Every line in a Dockerfile is an instruction to the container runtime about what the image should contain and how it should behave. From a dockerfile security perspective, the cumulative effect of those instructions is what determines the image's threat profile.

The FROM instruction sets the ceiling. Whatever vulnerabilities exist in the base image, you inherit. If you start from a general-purpose OS image that includes compilers, package managers, curl, wget, and dozens of system utilities, all of those are present in your final image whether you need them or not. A base image carrying 200 CVEs means your application starts life carrying 200 CVEs before you add a single dependency.

Every RUN instruction is an opportunity for accumulation. Each command that installs a package, fetches a script, or writes to the filesystem either expands the attack surface or - if done carefully - constrains it. apt-get install followed by cleanup in the same layer is very different from leaving the package cache in place. An image with leftover build tools, cached files, or temporary credentials is a larger image with more to attack.

The USER instruction determines privilege level at runtime. An image with no USER instruction runs as root by default. That means any process inside the container, including one that has been compromised, operates with root-level permissions - which makes escaping the container or escalating privilege within it considerably easier.

The order and structure of instructions affects what ends up in the final image. Multi-stage builds, layer ordering, and what gets included in the final stage all determine whether build-time tools make it into the production image. A single-stage Dockerfile that installs compilers to build an application, then serves that application in production, carries the compilers into production. A multi-stage Dockerfile does not.

Understanding these connections - between Dockerfile instruction and runtime exposure - is what separates teams that write Dockerfiles from teams that write secure Dockerfiles. For a comprehensive view of how these decisions connect to broader hardening strategy, our guide on container hardening techniques covers the full picture beyond the build stage.

The Dockerfile decisions that move the needle most on security

Not all Dockerfile decisions carry equal weight. These are the ones that produce the largest reduction in attack surface.

1. Start from the smallest base image that works

The single highest-impact dockerfile best practices decision is base image selection. A minimal base image - scratch, distroless, or a stripped-down Alpine variant - contains only what the application needs to run. There are no package managers to exploit, no shells to spawn, no debugging utilities to abuse. The CVE surface is a fraction of what a general-purpose OS image carries.

Using a minimal docker image as your starting point means every subsequent layer adds intentionally, rather than inheriting inadvertently. If your application is a compiled binary with no runtime dependencies, FROM scratch delivers an image with essentially zero inherited attack surface.

2. Run as a non-root user

Defaulting to root is one of the most common and consequential mistakes in container configuration. Setting a docker non root user with the USER instruction - or creating a dedicated user and group during the build - ensures that the application process runs with the minimum permissions it actually needs. This limits what a compromised process can do, both inside the container and in any escape attempt.

This is a small change in the Dockerfile that has significant consequences for the blast radius of any exploit.

3. Use multi-stage builds

Multi-stage builds let you use a full build environment in early stages and copy only the compiled output into a minimal final image. Build tools, compilers, test dependencies, and intermediate files stay in build stages that are discarded - only the final stage ships.

The production image contains the binary and nothing else. No Go toolchain. No source code. No build cache. This is one of the most effective ways to reduce docker image size while simultaneously reducing attack surface - the two goals are the same operation.

4. Pin versions explicitly

FROM ubuntu:latest resolves to a different image every few weeks. RUN apt-get install python3 installs whatever version is current. Both introduce silent drift - your builds stop being reproducible, and a new vulnerability in an upstream package enters your image without any deliberate decision on your part.

Pin your base image to a specific digest or tag. Pin package versions in install commands. This makes changes visible and intentional rather than invisible and automatic.

5. Set a read-only filesystem where possible

Adding --read-only at runtime - or structuring your Dockerfile so the application has no reason to write outside designated directories - limits what an attacker can do if they gain code execution. They cannot write malicious scripts, modify application files, or persist changes to the image.

6. Avoid storing secrets in the image

Build arguments passed with --build-arg, environment variables set with ENV, and files copied with COPY all persist in image layers - including intermediate ones - unless explicitly handled. Use Docker BuildKit secrets (RUN --mount=type=secret) for anything sensitive, and never copy credential files into a layer that ships.

The unintentional patterns that quietly introduce risk

Good intentions at the Dockerfile level are regularly undermined by patterns that seem innocuous but compound into meaningful exposure.

Leaving build tools in the final image. Installing gcc, make, or git during the build and not removing them - or not using a multi-stage build - means these tools are present in production. A compiler or shell in a production container significantly increases what an attacker can do with even limited access.

Using latest tags on base images. As noted above, latest is not a version - it is a pointer that moves. Images built weeks apart from the same FROM instruction may contain entirely different packages. Pinning is not optional if reproducibility and predictability matter to your security posture.

Installing packages without cleaning up in the same layer.

The package cache left in a separate layer is still present in the image, accessible to anyone who extracts it. Both the update and install commands should run in a single RUN instruction, with the cache cleaned in the same step.

Broad COPY instructions. COPY . . copies everything in the build context - including .git directories, local environment files, test data, and configuration files that contain secrets. Use a .dockerignore file aggressively. Only copy what the application actually needs.

Excessive capabilities. Some applications request NET_ADMIN, SYS_PTRACE, or CAP_SYS_ADMIN because they were granted it once and never questioned. Each capability granted is a privilege that a compromised process inherits. Drop all capabilities by defaul and add back only what the application requires.

Not defining HEALTHCHECK. While not a direct vulnerability, a missing HEALTHCHECK means orchestrators cannot distinguish a silently failed or compromised container from a healthy one - reducing your ability to detect and respond to incidents.

For a broader inventory of the most common Docker-level mistakes and how to address them systematically, our guide on best practices to manage Docker risk and vulnerabilities goes deeper on the operational side.

Keeping security intact after the Dockerfile is written

A Dockerfile that produces a clean image today will not necessarily produce a clean image in three months. Base images accumulate new CVEs as vulnerabilities are disclosed against packages they contain. Pinned dependency versions that were clean at build time develop new findings. The security built into a Dockerfile is a starting point, not a guarantee.

Scan at build time on every commit. Integrating a container scanner into CI/CD means every image is evaluated against current CVE databases before it can be promoted. This catches newly disclosed vulnerabilities in existing dependencies, not just problems that existed when the Dockerfile was first written. Our guide on container scanning best practices covers how to structure this scanning layer effectively.

Scan continuously in the registry. A clean image pushed to your registry today may have findings by next week. Registry-side scanning that re-evaluates images as new CVE data arrives ensures you have visibility into what is actually running in production - not just what was clean at build time.

Enforce policy gates. Scanning that produces reports but does not block deployments is not a control - it is a dashboard. Policy enforcement that fails pipelines on high and critical findings, and requires exceptions for any image that does not pass, is what converts scanning from a visibility tool into an actual security gate.

Treat the base image as a dependency. The same way you manage application library versions, the base image version should be tracked, evaluated for new findings regularly, and updated on a cadence that reflects your risk tolerance. For enterprise teams where upgrading the base image version may trigger compatibility testing, backported CVE patching on the pinned version is the practical path to keeping images clean without introducing breaking changes.

Signals that your approach is actually working

Dockerfile security practices are working when the results are measurable and consistent, not just when the process looks correct.

Your scanner finds fewer findings over time. If the CVE count in your images is stable or declining despite ongoing software development, your build practices are holding. If it trends upward regardless of scanning, something in the Dockerfile or base image update process is not working.

Findings cluster in application dependencies, not the base image. A well-configured Dockerfile with a minimal base should push almost all remaining CVE findings into application-layer packages - where they can be addressed through dependency management. If base image findings dominate, the base image selection or update process needs attention.

Images pass customer scans without waivers. When enterprise customers or auditors scan your delivered artifacts and find zero high and critical findings without you having to explain exceptions, it confirms that your build practices are producing provably clean output - not just internally acceptable output.

New CVE disclosures do not cause emergency responses. When a high-profile CVE is disclosed and your team can determine within minutes whether you are affected - because your SBOM is accurate and current and your images are already clean - that is a signal that your Dockerfile practices and ongoing scanning are functioning as a system, not just as individual tools.

Build sizes are declining and stabilizing. Consistently reducing docker image size through multi-stage builds and minimal base selection has a direct security correlate: smaller images have less to scan, less to attack, and less to audit. If image sizes are large and growing, the Dockerfile is likely accumulating rather than constraining.

FAQ

Do best practices differ for development versus production Dockerfiles?

Yes, meaningfully. Development Dockerfiles often benefit from including debugging tools, shell access, and hot-reload capabilities that would be inappropriate in production. The recommended approach is multi-stage builds or separate Dockerfile variants per environment, ensuring build-time and debug tooling never reaches the production image. Production images should always apply minimal base, non-root user, and read-only filesystem practices, even when development images relax these constraints.

How do Dockerfile choices affect compliance audit results?

Directly and significantly. Auditors evaluating container security - for SOC 2, FedRAMP, PCI-DSS, or ISO 27001 - examine base image CVE counts, privilege configuration, secrets handling, and whether images are signed and attested. Dockerfiles that produce images running as root, containing unnecessary packages, or carrying high-severity CVEs generate findings that require remediation or formal exceptions. Clean Dockerfile practices produce clean audit results with no waivers required.

Can poor Dockerfile habits undo runtime security controls?

Yes. Runtime controls like seccomp profiles, network policies, and read-only mounts constrain what a container can do, but they cannot remove packages or capabilities that were baked into the image. A container running as root with a shell and package manager available provides an attacker with far more options even inside a restricted runtime environment. Dockerfile security and runtime security are complementary - poor build practices create a larger surface that runtime controls have to compensate for, often imperfectly.

Is there a standard benchmark for evaluating Dockerfile security?

The CIS Docker Benchmark is the most widely referenced standard, covering base image practices, user configuration, filesystem settings, and runtime parameters. NIST SP 800-190 (Application Container Security Guide) provides a broader framework for container security governance. Most container scanners - including Trivy, Grype, and Anchore - can evaluate images against CIS benchmark controls as part of their scanning output, making compliance assessment automatable rather than manual.

What are the 7 blind spots in your vulnerability scans?

Discover when "0 vulnerabilities" doesn't actually mean you're clean.

Read now →

Ready to eliminate vulnerabilities at the source?