Enterprise DevSecOps & Automated CompliancePlaybook3 min readUpdated September 2026

What a Container Hardening Pass Actually Catches, Walked Through on a Real Image

Container hardening is easier to understand as a sequence applied to one real image than as an abstract checklist. Here's that sequence, walked through against a typical application container, with what each step actually removes as an attack path.

Vendors Covered in this Article

Disclosure: We may earn a commission if you buy through some links on this page. It doesn't change what we recommend.

Start: a working image with no hardening applied

The starting point is a common pattern: a full base operating system image, the application running as root, a handful of build tools left in the final image because they were needed during the build stage, and a container that can write to most of its own filesystem. None of this is unusual; it's the default outcome of writing a Dockerfile that just works, without a second pass focused on what's actually needed at runtime.

Step 1: switch to a minimal base image

Moving from a full operating system base to a minimal one removes the shell, package manager, and utilities an attacker would use to explore the container after gaining any foothold. This is usually the single most effective change to make first, because it doesn't just reduce the attack surface, it removes the tools an attacker needs to pivot from an initial compromise into further access.

The tradeoff is debuggability: a minimal image is harder to shell into for troubleshooting. Keep a separate debug variant of the image, used only in non-production environments, rather than compromising the hardened production image for convenience.

Step 2: run as a non-root user

A process running as root inside a container that later escapes to the host, through a container runtime vulnerability or a misconfigured volume mount, hands an attacker root on the host too. Setting a non-root user in the Dockerfile means that same escape lands with far more limited privileges, turning a full compromise into a contained one.

This step often surfaces file permission issues that root silently papered over, which is worth treating as useful information: those are exactly the paths worth double-checking for correctness rather than annoyances to route around.

For example, a team switches its image to a non-root user and the application immediately fails on startup because it tries to write a cache file into a directory owned by root. The tempting fix is to loosen permissions or go back to root. The better fix is to create a dedicated writable directory, assign it to the application user, and mount it as the one place the process may write. That change both resolves the error and documents where the application writes, which makes the later read-only filesystem step much easier to apply.

Step 3: make the filesystem read-only where possible

Setting the container's root filesystem to read-only, with explicit writable volumes only where the application genuinely needs to write (a temp directory, a specific cache path), closes off a whole category of persistence technique: an attacker who gains code execution can no longer drop a file onto the container's disk to survive a restart or pivot further.

This step requires actually knowing where your application writes, which is itself a useful exercise; several teams discover their application was writing logs or temp files to unexpected locations only once a read-only filesystem made those writes fail loudly instead of silently succeeding.

Step 4: strip build-time dependencies from the final image

A multi-stage build that compiles or installs dependencies in one stage and copies only the finished artifact into a clean final stage removes compilers, package caches, and build tools that have no runtime purpose but do have a history of security advisories of their own. Every tool left in the final image is one more thing that needs patching, whether or not the application ever uses it.

What's left after the four steps, and what still needs ongoing attention

The result of these four steps together is an image with a meaningfully smaller attack surface, a limited blast radius if the application is compromised, and no leftover tooling for an attacker to use once they're in, at very little cost beyond the initial Dockerfile rework. None of it is a one-time fix, though: a hardened image built today accumulates the same kind of drift as anything else, as dependencies age and new advisories land against packages that were clean when the image was built.

Treat the hardened Dockerfile as a template to reapply, not a finished artifact, and rebuild images on a schedule even when the application code hasn't changed, so patched base images and dependencies flow through automatically instead of waiting for the next feature deploy to happen to pull them in.

Apply this sequence to every image you ship:

  1. Switch to a minimal base image, keeping a separate debug variant for non-production environments only.
  2. Run the application as a non-root user, and treat any file permission errors that surface as paths worth checking.
  3. Set the root filesystem to read-only, with writable volumes only where the application genuinely needs to write.
  4. Use a multi-stage build so compilers, package caches, and build tools never reach the final image.
  5. Rebuild images on a schedule, even without code changes, so patched base images and dependencies flow through automatically.
Executive Capability Standard

What Good Looks Like

Good container hardening means a minimal base image, a non-root runtime user, a read-only filesystem where possible, and no build-time tooling left in the final image.

Building The Capability (5-Stage Skill Ladder)

1. Learn:Read your container runtime's documentation on the specific privileges a root process inside a container retains relative to the host, so the risk of step two is concrete rather than assumed.
2. Do Manually:Apply the four-step sequence to your highest-risk image first, treating each step as a separate, reviewable change.
3. Delegate:Assign hardening review to whoever owns your CI pipeline, since that's where Dockerfile changes are already reviewed before they ship.
4. Automate:Add an image scanner to your CI pipeline that fails a build when a new image runs as root or leaves a writable root filesystem.
5. Buy:Bring in a dedicated container security platform once you're running enough distinct images that manual review of each one stops scaling.

How to Get Started

Disclosure: We may earn a commission if you buy through some links on this page. It doesn't change what we recommend.

Frequently Asked Questions

How much do these changes typically affect image build time or size?

Minimal base images and multi-stage builds usually shrink the final image significantly, which tends to speed up deploys rather than slow them down. Build time may increase slightly with a multi-stage setup, but it's a one-time cost against a permanent security improvement.

Do we need to harden every container, or just the ones facing the internet?

Prioritize internet-facing and privileged containers first, but don't skip internal ones entirely. An internal service is still a step in a lateral-movement chain if something upstream of it is compromised.

How do we check whether an image already follows these practices?

Run it through an image scanner that checks for root users, writable filesystems, and known vulnerabilities in installed packages. Treat the first scan as a baseline, not a pass or fail, since most existing images will show several findings the first time.

About the numbers

This guide doesn't quote a sourced benchmark. Figures in it are estimates or general guidance, so check them against your own numbers.

Related Guides