Container Security: How to Harden Your Docker Images
Harden Docker containers with minimal base images, non-root users, dropped capabilities, read-only filesystems, CVE scanning in CI, and Kubernetes Pod Security Standards.
Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

Your Container Is Only as Secure as Its Weakest Layer
Container security isn't a single tool or practice -- it's a layered defense that starts with your base image choice and extends through build pipelines, runtime configuration, and orchestrator policies. Most teams get the basics wrong: running as root, shipping full OS images with hundreds of known CVEs, and mounting the Docker socket into containers like it's no big deal.
I've audited containerized environments where a single compromised container led to full cluster takeover because nobody bothered to drop Linux capabilities or set a read-only root filesystem. These aren't exotic attacks -- they're the first things any penetration tester tries.
What Is Container Security?
Definition: Container security encompasses the practices, tools, and configurations used to protect containerized applications throughout their lifecycle -- from image building and registry storage to runtime execution and orchestration. It covers image hardening, vulnerability scanning, access control, network segmentation, and kernel-level isolation.
Hardening Your Docker Images: Step by Step
- Start with a minimal base image -- Alpine (7 MB), distroless (2 MB), or scratch (0 MB) instead of ubuntu or debian
- Pin your base image digest -- use
FROM alpine:3.19@sha256:abc123...instead of floating tags - Create a non-root user and switch to it with
USER - Drop all Linux capabilities and add back only what you need
- Set the filesystem to read-only and mount writable tmpfs where needed
- Scan for CVEs in CI before the image reaches any registry
- Never store secrets in the image -- use build-time secrets or runtime injection
Minimal Base Images: Your First Line of Defense
Every package in your base image is a potential attack vector. A standard Ubuntu image has hundreds of packages, most of which your application never uses. Each one can have vulnerabilities.
| Base Image | Size | Typical CVEs | Shell | Package Manager |
|---|---|---|---|---|
| ubuntu:22.04 | 77 MB | 50-100+ | bash | apt |
| debian:bookworm-slim | 74 MB | 30-80 | bash | apt |
| alpine:3.19 | 7 MB | 0-5 | ash | apk |
| gcr.io/distroless/static | 2 MB | 0-2 | None | None |
| scratch | 0 MB | 0 | None | None |
Pro tip: Distroless images from Google are purpose-built for security. They contain only your application and its runtime dependencies -- no shell, no package manager, no
curl. If an attacker gets code execution, they can't easily drop to a shell or download tools.
Running as Non-Root
By default, containers run as root (UID 0). This is the single most common security misconfiguration. If an attacker exploits a vulnerability in your app, they have root access inside the container -- and with certain misconfigurations, that can mean root on the host.
FROM node:20-alpine
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm ci --production
# Switch to non-root before CMD
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
Verifying Non-Root at Runtime
# Check which user the container is running as
docker exec my-container whoami
# Should NOT output "root"
# Check the process UID
docker exec my-container id
# uid=1000(appuser) gid=1000(appgroup)
Dropping Linux Capabilities
Linux capabilities split root's power into granular permissions. By default, Docker grants containers a subset of capabilities, but it's still more than most apps need. The secure approach: drop all capabilities and add back only the ones your application requires.
# Drop all capabilities, add back only NET_BIND_SERVICE
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE my-app:latest
# In Kubernetes Pod spec
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
Watch out: Never run containers with
--privilegedin production. This flag gives the container ALL capabilities, access to all host devices, and disables most security mechanisms. A privileged container can trivially escape to the host.
Read-Only Root Filesystem
A read-only root filesystem prevents attackers from writing malicious binaries, scripts, or configuration changes to the container's filesystem. Most applications only need to write to specific directories like /tmp or a data directory.
# Docker: read-only root with writable /tmp
docker run --read-only --tmpfs /tmp:rw,noexec,nosuid my-app:latest
# Kubernetes: read-only root with emptyDir for /tmp
containers:
- name: app
image: my-app:latest
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
CVE Scanning in CI
Every image you build should be scanned for known vulnerabilities before it reaches a registry. The goal is to catch CVEs early -- in the pull request, not in production.
Popular Scanning Tools Compared
| Tool | Type | Cost | CI Integration | Database |
|---|---|---|---|---|
| Trivy | OSS | Free | GitHub Actions, GitLab CI | Multiple (NVD, Alpine, etc.) |
| Grype | OSS | Free | GitHub Actions | Anchore feed |
| Snyk Container | SaaS | Free tier / $25+/dev/mo | All major CI | Snyk Vulnerability DB |
| Docker Scout | SaaS | Included with Docker sub | Docker CLI native | Multiple sources |
| AWS ECR Scanning | Cloud | Free (basic) / Inspector pricing | Native to ECR | Clair / Inspector |
# GitHub Actions: Scan with Trivy
- name: Scan image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: my-app:latest
format: table
exit-code: 1
severity: CRITICAL,HIGH
Pro tip: Set
exit-code: 1in your scanner config to fail the CI pipeline when critical vulnerabilities are found. A scan that only reports but never blocks is just security theater.
Secrets Management
Secrets in Docker images are a common and serious mistake. Once a secret is in an image layer, anyone who pulls the image can extract it -- even if you delete the file in a later layer.
What NOT to Do
# WRONG: Secret baked into the image
ENV DATABASE_URL=postgres://admin:password@db:5432/mydb
COPY .env /app/.env
What to Do Instead
- Build-time secrets (Docker BuildKit):
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci - Runtime environment variables: Pass via
docker run -eor Kubernetes Secrets - External secret stores: HashiCorp Vault, AWS Secrets Manager ($0.40/secret/month), or SOPS for encrypted files in Git
Kernel-Level Security: seccomp and AppArmor
Beyond container configuration, Linux kernel security modules provide additional isolation:
seccomp Profiles
seccomp restricts which system calls a container can make. Docker ships a default profile that blocks about 44 syscalls, but you can create custom profiles for tighter control.
# Run with Docker's default seccomp profile (enabled by default)
docker run --security-opt seccomp=default my-app:latest
# Run with a custom profile
docker run --security-opt seccomp=custom-profile.json my-app:latest
AppArmor Profiles
AppArmor provides mandatory access control, restricting what files and capabilities a process can access. Docker applies a default AppArmor profile to all containers on systems where AppArmor is available.
Kubernetes Pod Security Standards
Kubernetes v1.25+ replaced PodSecurityPolicy with Pod Security Standards (PSS) enforced via the Pod Security Admission controller. Three built-in levels:
| Level | What It Allows | Use Case |
|---|---|---|
| Privileged | No restrictions | System-level workloads (CNI, storage drivers) |
| Baseline | Blocks known privilege escalations | Most application workloads |
| Restricted | Hardened, follows current best practices | Security-sensitive workloads |
# Enforce restricted policy on a namespace
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
Pricing: Security Tools and Services
| Tool | Free Tier | Paid Pricing |
|---|---|---|
| Trivy | Fully free (OSS) | N/A |
| Snyk Container | 200 tests/month | From $25/dev/month (Team) |
| HashiCorp Vault | OSS self-hosted | HCP Vault from $0.03/hr |
| AWS Secrets Manager | None | $0.40/secret/month + $0.05/10K API calls |
| Sysdig Secure | None | Custom pricing (runtime security) |
Frequently Asked Questions
Is running as root inside a container dangerous if the container is isolated?
Yes. Container isolation isn't perfect -- kernel vulnerabilities, misconfigured volume mounts, or the Docker socket can allow container escapes. Running as root inside the container means root on the host if isolation breaks. Non-root is defense in depth: even if isolation fails, the attacker has limited privileges.
Do I need to scan images if I'm using Alpine or distroless?
Yes, always. Alpine has fewer CVEs than Ubuntu, but it still has them. Your application dependencies (npm packages, Python libraries, Go modules) also have vulnerabilities that base-image choice doesn't affect. Scan everything, every build.
What's the difference between seccomp and AppArmor?
seccomp filters system calls -- it controls what operations a process can ask the kernel to do. AppArmor controls file and resource access -- what paths a process can read, write, or execute. They complement each other. Docker enables default profiles for both on supported systems.
Should I use Kubernetes Pod Security Standards or a third-party policy engine?
Start with Pod Security Standards -- they're built in and cover the most important security baselines. If you need more granular control (custom policies, mutation, audit logging), add OPA Gatekeeper or Kyverno. Most teams don't need a policy engine until they have multiple teams deploying to the same cluster.
How do I handle secrets in Kubernetes?
Kubernetes Secrets are base64-encoded, not encrypted, by default. For production, enable encryption at rest in etcd, or better yet, use an external secret store (Vault, AWS Secrets Manager) with the External Secrets Operator to sync secrets into the cluster. Never commit secrets to Git in plain text.
Can Docker Scout replace Trivy or Snyk?
Docker Scout is a solid choice if you're already paying for Docker Desktop subscriptions. It integrates natively with docker build and provides remediation advice. However, Trivy is free, faster in CI, and supports more artifact types. For most teams, Trivy is the pragmatic default.
Conclusion
Container security isn't optional, and it's not something you bolt on after deployment. Use minimal base images, run as non-root, drop capabilities, set read-only filesystems, scan in CI, and manage secrets externally. These six practices eliminate the vast majority of container-related vulnerabilities. Start with the Dockerfile changes -- they take five minutes and make the biggest difference.
Written by
Abhishek Patel
Infrastructure engineer with 10+ years building production systems on AWS, GCP, and bare metal. Writes practical guides on cloud architecture, containers, networking, and Linux for developers who want to understand how things actually work under the hood.
Related Articles
Kubernetes Pods, Deployments, and Services: A Visual Guide
Kubernetes complexity concentrates in three core objects: Pods, Deployments, and Services. This visual guide explains what they do, how they connect, and what happens during rolling updates.
11 min read
ContainersHelm Charts Demystified: Kubernetes Templating Without the Pain
Master Helm charts: chart anatomy, Go templating, values overrides, public registries, release management, lifecycle hooks. Plus when to use Kustomize instead.
9 min read
ContainersWhat is a Service Mesh? Istio and Linkerd Explained Simply
Understand service mesh architecture with sidecar proxies and the data/control plane split. A detailed Istio vs Linkerd comparison covering performance, complexity, features, and when a mesh is justified.
8 min read
Enjoyed this article?
Get more like this in your inbox. No spam, unsubscribe anytime.