The container security best practices that actually move your risk needle are a short, boring list: build minimal images, run as a non-root user, scan what you ship, keep secrets out of images, and run with least privilege. Most container incidents trace back to skipping one of these container security best practices, not to some exotic zero-day.
The reason to focus on fundamentals is that containers concentrate risk in a few predictable places: what you put in the image, how the image runs, and what the runtime is allowed to do. Get those three right and you have closed the doors attackers actually use.
Start with a minimal base image
Every package in your image is attack surface and a potential CVE. A full operating system base drags in shells, package managers, and utilities your application never uses but an attacker gladly would. Shrink the image to what the app genuinely needs.
Prefer minimal or distroless bases. A multi-stage build lets you compile with a full toolchain and then copy only the runtime artifacts into a lean final image:
# build stage
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
# runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
USER 65532:65532
ENTRYPOINT ["/app"]
The smaller the image, the fewer packages a scanner will ever flag, the faster it pulls, and the less there is for an attacker to pivot through if they get a foothold.
Never run as root
By default a container process runs as root, and root inside a container is uncomfortably close to root on the host if any isolation weakness exists. Define an unprivileged user in the Dockerfile and run as it:
RUN addgroup --system app && adduser --system --ingroup app app
USER app
Pair this at runtime by dropping Linux capabilities you do not need, disabling privilege escalation, and mounting the root filesystem read-only where possible. In Kubernetes that is a security context:
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
Most applications need almost none of the default capabilities. Dropping them all and adding back only what breaks is a fast, high-value hardening step.
Scan images and keep scanning
Building from a clean base today does not keep it clean. Vulnerabilities get disclosed against packages already inside your image after you built it. So scanning is two jobs: scan in CI to stop new problems from shipping, and scan images at rest in your registry continuously so newly disclosed CVEs against existing images surface promptly.
Fail the build on high-severity, fixable vulnerabilities, and generate an SBOM for each image so you can answer "which images contain package X?" the moment an advisory lands. A composition analysis tool such as Safeguard's SCA can maintain that inventory and re-evaluate it against new advisories automatically, which turns the next big CVE from a manual hunt into a query.
Keep secrets out of the image
A credential baked into an image layer is there forever, recoverable by anyone who can pull the image, even if a later layer deletes it. Never COPY a secrets file or bake a key into an ENV. Inject secrets at runtime through your orchestrator's secret store or a dedicated secrets manager, and scan your images and source for accidentally committed credentials. Treat any secret that ever landed in an image as compromised and rotate it.
Pin and verify what you pull
Base images referenced by a floating tag like :latest change under you, which is bad for both reproducibility and security. Pin by digest so you always get the exact bytes you tested:
FROM alpine@sha256:... # pinned digest, not :latest
Pull from trusted registries and verify image signatures where your platform supports it. Supply chain attacks increasingly target the images teams pull rather than the code they write, so knowing precisely what you are running is a real control, not a nicety.
Constrain the runtime
Even a perfectly built image can do damage if the runtime lets it. Apply the same least-privilege thinking to how containers run: set CPU and memory limits so a compromised or runaway container cannot exhaust the node, isolate workloads with network policies so a breached container cannot freely reach everything else, and avoid mounting the Docker socket into containers, which effectively grants host control. Keep the container runtime and orchestrator patched, since escapes usually live in that layer rather than in your app.
Make it automatic
These practices only hold if they are enforced rather than remembered. Encode them as policy in CI: fail builds that run as root, that use unpinned base images, or that carry high-severity fixable CVEs. Admission controllers in Kubernetes can reject workloads that request privileges they should not have. The goal is that doing the secure thing is the path of least resistance and the insecure thing is what gets blocked. Our academy has a container hardening track that walks through wiring these gates in.
FAQ
What is the single most important container security practice?
If forced to pick one, do not run as root and drop all capabilities you do not need. Combined with a minimal base image, this closes the most common escalation paths. Running as an unprivileged user with a read-only root filesystem dramatically limits what a compromised container can do.
Why use distroless or minimal base images?
Every package in an image is potential attack surface and a possible CVE. Minimal and distroless images strip out shells, package managers, and unused utilities, so there is far less for a scanner to flag and far less for an attacker to exploit if they get in. They also pull faster.
How often should I scan container images?
Scan in CI to block new issues, and scan images at rest in your registry continuously. New vulnerabilities are disclosed against packages already inside built images, so continuous re-scanning against current advisories is what catches those before an attacker does.
How should secrets be handled in containers?
Never bake secrets into image layers, since they remain recoverable even if a later layer deletes them. Inject secrets at runtime via your orchestrator's secret store or a secrets manager, scan for accidentally committed credentials, and rotate any secret that ever touched an image.