Safeguard
AI Security

Docker and Containers: A Practical Security Guide

Docker and containers share the host kernel, so their security model is different from virtual machines. Here is how to build, run, and scan containers safely.

Safeguard Research Team
Research
7 min read

Dockers and containers get talked about as if they were airtight boxes, but the security reality is more nuanced: a container is a process on the host, isolated by kernel features rather than a hardware boundary, which means its security depends on how you build the image and how you run it. Understanding that distinction is the difference between a hardened deployment and a false sense of safety. This guide walks through where container risk actually lives, from the image you build to the runtime privileges you grant, and how to keep it under control.

First, a terminology note, because the phrase "dockers and containers" mixes two things. Docker is a specific toolchain and runtime for building and running containers. A container is the general concept: an isolated user-space instance sharing the host operating system's kernel. Docker popularized containers but is not the only implementation; containerd, CRI-O, and Podman run the same OCI-standard images. What follows applies to containers broadly, with Docker as the concrete example.

The isolation model, and why it matters

A virtual machine runs its own kernel on virtualized hardware, and the hypervisor enforces a hard boundary between guests. A container does not. It shares the host kernel and is isolated by Linux primitives, namespaces, which give each container its own view of processes, network, and filesystem, and cgroups, which limit its resource usage. Capabilities and seccomp profiles further restrict what system calls a container can make.

The consequence is direct: if an attacker escapes the container's isolation, they are on the host kernel, and a kernel vulnerability that allows a container escape puts every container on that host at risk. This is not theoretical; container-escape CVEs surface periodically in the runtime and the kernel. It is why "containers are not a security boundary as strong as VMs" is a fair summary, and why defense in depth, hardened images plus restricted runtime plus a patched host kernel, is the right posture rather than trusting isolation alone.

Start with a minimal base image

Most of a container's vulnerability count comes from its base image, not the application code you add. A full distribution base like a general-purpose Debian or Ubuntu image ships hundreds of packages, most of which your application never uses, and every one of them is a potential CVE and a potential tool for an attacker who gets a shell. The single highest-leverage decision in container security is choosing a small base.

Minimal bases, Alpine, distroless images that contain only your app and its runtime dependencies, or a "slim" variant, cut the attack surface dramatically. A distroless image has no shell and no package manager, which means an attacker who achieves code execution cannot simply run standard tooling to explore or escalate. The tradeoff is debugging convenience, since you cannot exec into a shell that does not exist, but the security gain is large and there are ephemeral-debug patterns that recover the convenience when you genuinely need it.

Do not run as root

By default, a process inside a container runs as root, and while namespaces mean that is not quite the host's root, it is far more privilege than most applications need and it materially widens the blast radius of any escape. Create a non-root user in the Dockerfile and switch to it with a USER directive before the entrypoint. Combine that with a read-only root filesystem where the app allows it, so a compromise cannot write a payload to disk, and mount a small writable tmpfs only for the paths that genuinely need it.

Then trim capabilities. Containers get a default set of Linux capabilities, and most workloads need almost none of them. Drop all capabilities and add back only the specific ones required. Never run with the --privileged flag in production; it disables most of the isolation that makes containers safe in the first place, and it is a recurring finding in real-world container breaches.

Scan images, and scan them where they are built

A container image is a stack of layers, and each layer can introduce packages with known vulnerabilities. Image scanning inventories the operating-system packages and application dependencies in an image and matches them against vulnerability databases, the same software composition analysis idea applied to the full image rather than just your manifest. Run the scan in the build pipeline so a vulnerable image never reaches a registry, and re-scan images already in the registry on a schedule, because a CVE disclosed today applies to an image you built and pushed last month without any change on your part.

The practical discipline is to gate on severity and fixability. Block the build on critical and high vulnerabilities that have an available fix, since those you can act on immediately by rebuilding on a patched base. Track, rather than block on, vulnerabilities with no fix available yet, so an unfixable OS-package CVE does not permanently wedge your pipeline. An SCA tool such as Safeguard can flag which findings have upstream fixes and which do not, which is the distinction that makes the gate usable rather than a source of constant overrides.

Lock down the supply chain of the image itself

Beyond the packages inside, the image as an artifact has its own supply-chain risk. Pin base images by digest rather than a floating tag like latest, so a rebuild uses the exact image you tested rather than whatever the upstream maintainer pushed since. Sign your images and verify signatures at deploy time so the cluster runs only images your pipeline produced, not a look-alike an attacker pushed to the registry. Generate a software bill of materials at build time and keep it, so when the next widely-exploited CVE lands you can query which images contain the affected component in seconds instead of rebuilding everything to find out.

Runtime is not the end

Building a clean image is necessary but not sufficient, because vulnerabilities are disclosed continuously and a container that was clean at build time drifts. Keep the host kernel patched, since that is the boundary an escape has to cross. Limit what containers can reach on the network with network policies rather than a flat, everything-can-talk-to-everything default. And prefer immutable deployments: rebuild and redeploy to pick up patches rather than exec-ing into a running container to update packages in place, which produces snowflake containers that no longer match any image you can scan.

FAQ

Are containers as secure as virtual machines?

By default, no. VMs enforce isolation at the hardware level via a hypervisor, while containers share the host kernel and rely on kernel features for isolation. A kernel vulnerability can allow a container escape. Hardened images, restricted runtime privileges, and a patched host narrow the gap considerably.

What is the most important container security step?

Two things tie for first: use a minimal base image to shrink the vulnerability surface, and run as a non-root user with dropped capabilities to limit the blast radius of any compromise. Both are cheap and high-impact.

Do I need to scan container images if I scan my code?

Yes. Code scanning misses the operating-system packages and runtime layers baked into the base image, which is where much of a container's vulnerability count lives. Image scanning covers the whole artifact.

What does the --privileged flag do?

It disables most of the isolation and capability restrictions that make a container reasonably safe, effectively giving the container near-host-level access. Avoid it in production; it is a frequent root cause in real container breaches.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.