Docker container security has three layers: image scanning (Trivy, Snyk, Grype) at build time to catch CVEs in base images and dependencies, runtime hardening (read-only filesystem, no privileged, drop capabilities, non-root user) to limit what a compromised container can do, and runtime monitoring (Falco, Tetragon, Aqua) to detect active attacks. The team that does all three has defense in depth. The team that does only one has a single layer of defense.
Table of contents
- Layer 1: Image scanning
- Layer 2: Runtime hardening
- Layer 3: Runtime monitoring
- Supply chain: SBOMs and signing
- Network policies and isolation
- FAQ
Layer 1: Image scanning
Image scanning looks at the base image, OS packages, and language dependencies for known CVEs.
Tools:
- Trivy (open-source, Aqua): Scans OS packages, language deps, IaC, K8s manifests.
- Snyk: Commercial with a generous free tier. Scans images and dependencies.
- Grype (open-source, Anchore): Fast scanner, similar coverage to Trivy.
- Docker Scout: Built into Docker Hub and Docker Desktop.
- Clair (open-source, Quay): Used by Quay registry.
- AWS ECR Scan, GCP Artifact Registry Scan: Cloud-provider integrated scanning.
Integrate into CI/CD:
trivy image myapp:1.0.0 --severity HIGH,CRITICAL --exit-code 1
The team that runs this in CI fails builds on HIGH/CRITICAL CVEs. The team that scans only periodically ships vulnerable images.
Layer 2: Runtime hardening
A hardened container:
- Runs as non-root user.
- Has read-only root filesystem.
- Drops all Linux capabilities, adds back only what’s needed.
- Has no privilege to access the host (no privileged flag, no hostPath, no hostNetwork).
- Has resource limits (CPU, memory, PIDs).
- Uses a minimal base image (Alpine, distroless, scratch).
Dockerfile example:
FROM gcr.io/distroless/static-debian12
COPY --chown=nonroot:nonroot myapp /myapp
USER nonroot
ENTRYPOINT ["/myapp"]
Kubernetes pod spec:
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
runAsUser: 1000
The team that applies this to every workload has a consistent security baseline. The team that hardens some workloads and not others has inconsistent blast radii.
Layer 3: Runtime monitoring
Runtime monitoring watches syscalls, network connections, file changes for indicators of attack.
- Falco (open-source, Sysdig): The de facto standard. Detects shell in container, unexpected network, file changes.
- Tetragon (open-source, Cilium): eBPF-based, lower overhead than Falco.
- Aqua Runtime: Commercial runtime security.
- Sysdig Secure: Commercial, Falco + more.
Example Falco rule:
- rule: Shell in container
desc: Detect shell spawned in container
condition: container and proc.name in (bash, sh, zsh)
output: Shell in container (user=%user.name command=%proc.cmdline container=%container.name)
priority: WARNING
The team that uses Falco/Tetragon catches active attacks. The team that only scans images catches vulnerabilities but not exploits in the wild.
Supply chain: SBOMs and signing
Beyond the three layers, supply chain security:
- SBOM (Software Bill of Materials): List of all components in the image. Tools: Syft, CycloneDX, SPDX.
- Image signing: cosign (Sigstore), Docker Content Trust. Verify the image hasn’t been tampered with.
- Provenance: SLSA framework, attestations about how the image was built.
The team that signs images and verifies on pull ensures no one swapped the image between registry and runtime.
Network policies and isolation
Even within Docker (and especially in K8s), network segmentation:
# Kubernetes NetworkPolicy: default deny + allow only needed traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
The team that uses default-deny + explicit allow has the right baseline. The team that allows all-to-all is one compromised container away from lateral movement.
FAQ
What is the most important Docker security step?
Don’t run as root. USER nonroot in the Dockerfile, or securityContext.runAsNonRoot: true in K8s. The team that fixes this one thing eliminates the most common container-escape vector.
Should I use Alpine or distroless?
Distroless for production (smaller attack surface, fewer CVEs). Alpine for dev or when you need a shell for debugging. The team that uses scratch for pure Go binaries has the smallest possible image but no shell for debugging.
How often should I rescan images?
Continuously. New CVEs are published daily. Tools like Trivy, Snyk, and ECR scan can rescan on every push and on a schedule. The team that scans once at build misses CVEs published later.
What about Docker socket access?
Never mount the Docker socket (/var/run/docker.sock) into a container. It’s equivalent to root on the host. The team that needs container orchestration uses K8s, not Docker-in-Docker.
Is rootless Docker enough?
Rootless Docker (running the daemon as non-root) is much safer than the default. But it has limitations (some networking features don’t work, cgroups v1 vs v2 issues). The team that needs full hardening uses rootless + the other layers above.
If you are sizing the infrastructure for the kind of project this post covers, the RunxBuild hosting calculator is the right place to model the line items. The compute, the memory, the storage, the bandwidth, the database - each one is a separate number, and the team’s mental model for the platform is the sum of those numbers. The RunxBuild dashboard is where the team sees the actual usage in one place.
Useful related references: