Containers provide process isolation and reproducible deployments, but they introduce their own security challenges. A container image is a snapshot of all the software your application depends on — including every vulnerable library, every misconfigured service, and every secret accidentally included during the build. Securing containers requires attention at build time, runtime, and throughout the supply chain.
Why Base Image Choice Matters
The most impactful security decision in your Dockerfile is the choice of base image. A large base image like ubuntu:latest or debian:latest contains hundreds of packages, many of which are unnecessary for your application — and each one is a potential vulnerability.
# High attack surface — hundreds of packages, many vulnerabilities
FROM ubuntu:latest
# Much better — minimal image, dramatically fewer CVEs
FROM python:3.12-alpine
# Best for production — distroless contains only your app and runtime
FROM gcr.io/distroless/python3
Alpine Linux images are typically 5-10x smaller than Debian-based images and have significantly fewer vulnerabilities. Distroless images (from Google) contain only the language runtime and your application — no shell, no package manager, no utilities. This makes them extremely difficult to exploit even if a vulnerability exists, because there is no shell for an attacker to drop into.
Never Run as Root
By default, Docker containers run as root (UID 0) unless you specify otherwise. If an attacker exploits a vulnerability in your application, they have root access inside the container. While containers are isolated from the host, root inside a container is a significant privilege escalation vector for container escape attacks.
# Wrong — runs as root
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# Correct — create a non-root user and switch to it
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Create app user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --chown=appuser:appgroup . .
CMD ["node", "server.js"]
At runtime, enforce non-root with a security context if deploying to Kubernetes: runAsNonRoot: true and runAsUser: 1000.
Read-Only Filesystems
Running a container with a read-only root filesystem prevents an attacker from writing malware, modifying configuration files, or establishing persistence on the filesystem. Most applications do not need to write to the filesystem at all (logs go to stdout, data goes to a volume).
# Docker run — read-only root filesystem
docker run --read-only \
--tmpfs /tmp \ # Allow writes to /tmp
--tmpfs /var/run \ # For PID files if needed
myapp:latest
# Docker Compose
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp
Secrets Must Not Be in ENV or Layers
The two most common ways secrets end up in Docker images:
- Using
ENV SECRET_KEY=abc123in the Dockerfile — the value is baked into the image layer and visible to anyone who runsdocker inspectordocker history - Copying a
.envfile withCOPY . .and forgetting to add it to.dockerignore
# .dockerignore — always include these
.env
.env.*
*.key
*.pem
*.p12
secrets/
.git/
Use Docker secrets (in Swarm), Kubernetes secrets (mounted as files, not environment variables), or external secret managers (AWS Secrets Manager, HashiCorp Vault) to inject secrets at runtime without baking them into the image.
Multi-Stage Builds Reduce Attack Surface
Multi-stage builds let you compile or build your application in one stage and copy only the output to the final image, leaving behind all build tools, compilers, test dependencies, and source code.
# Build stage — has all build tools
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
# Final stage — has only the binary
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
The final image contains only the static binary and the distroless base — no Go toolchain, no source code, no shell. An attacker who exploits a vulnerability in the application has almost nothing to work with.
Image Scanning Tools
Scan your images for known CVEs as part of your CI/CD pipeline. Every unscanned image pushed to production is an unknown risk.
- Trivy (Aqua Security, open source) — scans images, filesystems, and git repositories for vulnerabilities, misconfigurations, and secrets. Fast and accurate:
trivy image myapp:latest - Docker Scout — built into Docker Desktop and Docker Hub; provides CVE data and fix recommendations directly in the Docker workflow
- Snyk Container — integrates with CI/CD pipelines and provides fix PRs for vulnerable base images
- Grype (Anchore, open source) — vulnerability scanner that works with SBOM outputs from Syft
# Add Trivy to your GitHub Actions pipeline
- name: Scan Docker image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'table'
exit-code: '1' # Fail the build on critical vulnerabilities
severity: 'CRITICAL,HIGH'
COPY vs ADD: A Security Difference
Both COPY and ADD copy files into the image, but ADD has two additional behaviors that introduce security risks:
- It can download files from remote URLs — introducing a network dependency and potential for serving malicious content
- It automatically extracts tar archives — which can cause unintended path traversal if the archive contains
../paths
Use COPY by default. Use ADD only when you specifically need tar extraction, and never with remote URLs.
Dropping Linux Capabilities
Docker containers inherit a set of Linux capabilities by default that grant elevated privileges. Drop all capabilities and add back only those your application requires:
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp:latest
# In Kubernetes securityContext:
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Only if binding to ports < 1024
Most web applications need zero extra capabilities. Running with --cap-drop ALL and a non-root user, read-only filesystem, and no new privileges (--security-opt no-new-privileges) creates a dramatically hardened container.