Docker Security Best Practices
Docker Security Best Practices: Securing Your Containerized Workloads
Docker has revolutionized how we build, ship, and run applications, but containerization introduces a unique security surface area that demands careful attention. A misconfigured container can expose sensitive data, enable privilege escalation, or become a pivot point for broader infrastructure compromise. This article covers practical, actionable security best practices for Docker environments — from image construction to runtime hardening — so you can ship fast without sacrificing safety.
1. Minimize the Attack Surface with Slim Base Images
The smaller your base image, the fewer vulnerabilities it contains. Large images like ubuntu or centos carry thousands of packages — many of which are unnecessary and potentially vulnerable.
Prefer Alpine-based or distroless images whenever possible. Distroless images from Google contain only your application and its runtime dependencies, stripping away package managers, shells, and utilities that attackers commonly leverage.
# ❌ Avoid: bulky base image
FROM ubuntu:22.04
# ✅ Prefer: minimal base image
FROM alpine:3.19
# ✅ Best: distroless for production
FROM gcr.io/distroless/base-debian12
When using alpine, be aware that it uses musl libc instead of glibc. Some applications may require additional compatibility layers or should be tested thoroughly before switching.
2. Build Secure Images with Multi-Stage Builds
Multi-stage builds let you compile and build artifacts in one image while shipping only the final, stripped-down result. This eliminates build tools, compilers, and source code from the production image entirely.
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp .
# Production stage
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/myapp /myapp
RUN chmod +x /myapp
USER nobody
ENTRYPOINT ["/myapp"]
The resulting image contains only the compiled binary, CA certificates, and timezone data — no Go toolchain, no source files, no package manager.
3. Never Run Containers as Root
Running processes inside a container as root is one of the most common and dangerous misconfigurations. If an attacker escapes the container, they gain root access on the host. Always switch to a non-root user.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Create a non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
COPY --chown=nodejs:nodejs . .
EXPOSE 3000
CMD ["node", "index.js"]
Additionally, enforce this at the daemon level by adding the following to /etc/docker/daemon.json:
{
"no-new-privileges": true
}
This prevents any container process from gaining additional privileges through setuid or setgid binaries. You can also use a seccomp profile to restrict system calls:
docker run --security-opt seccomp=/path/to/profile.json myapp
4. Keep Images Updated and Scan Regularly
Vulnerabilities are discovered constantly. An image built today may be compromised tomorrow. Establish a routine of scanning your images and updating base images on a regular cadence.
Automated Scanning in CI/CD
Integrate scanning directly into your pipeline so vulnerable images never reach production.
# GitHub Actions example
name: Docker Security Scan
on: [push]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'sarif'
exit-code: '1'
severity: 'HIGH,CRITICAL'
Recommended Scanning Tools
| Tool | Type | Notes |
|---|---|---|
| Trivy | Image & config scanner | Fast, supports Kubernetes, IaC, and secrets |
| Grype | Image scanner (Syft) | Great SBOM generation alongside scanning |
| Dive | Image inspector | Visualize each layer to spot bloat and issues |
| Clair | Vulnerability classifier | Works well with CVE databases in large fleets |
5. Protect Sensitive Data with Build Secrets and Runtime Volumes
Hardcoding credentials, API keys, or certificates inside an image layer is a critical mistake. Image layers are cached and often shared across registries, meaning secrets can leak indefinitely.
Build Secrets (BuildKit)
# syntax=docker/dockerfile:1
FROM alpine:3.19
RUN apk add --no-cache curl
# Securely inject secrets at build time without persisting them
RUN --mount=type=secret,id=token \
TOKEN=$(cat /run/secrets/token) \
curl -H "Authorization: Bearer $TOKEN" \
https://api.example.com/config -o /etc/app/config.json
SECRET token
Build with:
DOCKER_BUILDKIT=1 docker build \
--secret id=token,src=.secrets/token \
-t myapp .
Runtime Secrets via Mounts
Never bake secrets into images. Instead, inject them at runtime through mounted volumes or orchestration-native secret stores.
# Mount a secret file at runtime
docker run \
--secret my-db-password \
myapp
# Or use environment variables from a secured source
docker run \
-e DB_PASSWORD_FILE=/run/secrets/db_pass \
myapp
For Kubernetes deployments, leverage native Secret objects with encryption at rest and RBAC-controlled access.
6. Use Read-Only Filesystems and Immutable Containers
Making container filesystems read-only prevents attackers from writing malicious scripts, modifying configurations, or establishing persistence inside a running container.
# docker-compose.yml
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp
- /runtime
volumes:
- app-data:/var/data
- ./config.json:/etc/app/config.json:ro
volumes:
app-data:
// docker run flags equivalent
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /runtime:rw,noexec,nosuid,size=32m
Key points:
- Mount only the directories that need write access.
- Use
noexecon temporary mounts to prevent executing binaries from those paths. - Mount configuration files as read-only (
:ro) to prevent in-container modification.
7. Restrict Network Access and Isolate Containers
Containers share the host kernel, making network isolation essential. By default, Docker allows broad network communication between containers and the host.
Define Custom Networks
# docker-compose.yml
services:
frontend:
image: myapp-frontend:latest
networks:
- frontend-net
backend:
image: myapp-backend:latest
networks:
- frontend-net
- backend-net
database:
image: postgres:16-alpine
networks:
- backend-net
ports:
- "5432:5432" # Only accessible within backend-net
networks:
frontend-net:
driver: bridge
backend-net:
driver: bridge
The frontend service cannot reach the database directly — traffic must flow through the backend service, enforcing a clear network segmentation.
Disable Host Network Sharing
Never use network_mode: host unless absolutely necessary. It bypasses Docker's network isolation entirely, giving the container full access to the host's network stack.
8. Harden the Docker Daemon and Host OS
Container security starts at the host level. A compromised host means compromised containers, regardless of how well-secured the images are.
Daemon Configuration
Edit /etc/docker/daemon.json to enforce security policies:
{
"icc": false,
"no-new-privileges": true,
"userns-remap": "default",
"default-runtime": "runc",
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"live-restore": true
}
| Setting | Purpose |
|---|---|
icc: false | Disables unrestricted inter-container communication |
userns-remap | Maps container root to a non-privileged host user |
no-new-privileges | Blocks privilege escalation inside containers |
live-restore | Keeps containers running during daemon restarts |
User Namespace Remapping
User namespace remapping translates the container's root (UID 0) to an unprivileged UID on the host, dramatically reducing the impact of a container escape.
{
"userns-remap": "default"
}
After enabling this, Docker automatically creates a dockremap user on the host and remaps all container UIDs. Note that this requires adjusting volume mount permissions accordingly.
Keep the Host Updated
Apply kernel updates promptly. Container escapes often exploit kernel vulnerabilities, so keeping the host OS patched is one of the highest-impact security actions you can take.
9. Enforce Resource Limits and Avoid DoS
Unrestricted resource consumption can lead to denial-of-service conditions affecting the entire host or co-located workloads. Always define limits.
# docker-compose.yml
services:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
pids: 256
reservations:
memory: 256M
stop_grace_period: 30s
Or via CLI:
docker run \
--cpus=1.0 \
--memory=512m \
--pids-limit=256 \
--restart=unless-stopped \
myapp
Key limits to consider:
- CPU and memory prevent resource starvation of other workloads.
- PID limits mitigate fork-bomb attacks.
- Restart policies ensure availability without manual intervention.
10. Implement Runtime Monitoring and Logging
Detection is as important as prevention. Real-time monitoring catches anomalies — unexpected process launches, abnormal network connections, or filesystem changes — before they escalate.
Structured Logging with Rotation
Configure log drivers to send structured output to a centralized logging backend rather than letting logs accumulate locally.
// daemon.json
{
"log-driver": "fluentd",
"log-opts": {
"fluentd-address": "localhost:24224",
"fluentd-async": "true",
"tag": "docker.{{.Name}}"
}
}
Runtime Security Tools
| Tool | Capability |
|---|---|
| Falco | Runtime threat detection using system call auditing |
| Aqua MicroScanner | Lightweight runtime vulnerability scanning |
| Sysdig | Container behavior analysis and compliance |
| auditd | Host-level audit framework for filesystem and syscall monitoring |
Example Falco rule for detecting unexpected shell access:
- rule: Shell spawned in container
desc: Alert when a shell process starts inside a container
condition: >
spawned_process and container and
shell_procs and proc.name != tsa
output: >
Shell spawned in container (
user=%user.name container=%container.id
host=%host.name command=%proc.cmdline
parent=%proc.pname)
priority: CRITICAL
11. Supply Chain Security and Image Trust
Compromised base images or malicious dependencies in the supply chain are a growing threat. Verify the provenance of every image you pull.
Sign and Verify Images with Notary/Cosign
# Sign an image with Cosign
cosign sign --key cosign.key myregistry.io/myapp:1.0.0
# Verify the signature before deployment
cosign verify --key cosign.pub myregistry.io/myapp:1.0.0
Use Private Registries with Access Control
Host a private registry (e.g., Harbor, GitLab Container Registry) and restrict who can push and pull images. Combine this with image scanning at the registry level so that unscanned images cannot be promoted to production.
# Restrict image sources with kubelet admission
# Pod security policy or OPA/Gatekeeper constraint
apiVersion: constraints.policy.cert-manager.io/v1
kind: ImagePolicy
spec:
exemptImages: []
allowRegistries:
- "myregistry.io"
- "gcr.io/distroless"
denyRegistries:
- "docker.io"
Pin Image Versions
Never use :latest in production. Pin images to specific digests or semantic versions.
# ✅ Pinned by digest — immutable and reproducible
FROM alpine:3.19.1@sha256:a1f6d5d...
FROM myapp:2.4.1@sha256:b2c8e7f...
# ❌ Never do this in production
FROM myapp:latest
12. Leverage Security Profiles and Standards
Standards like CIS Docker Benchmark and NSA/CISA Kubernetes Hardening Guide provide structured checklists for securing containerized environments. Aligning your configurations against these frameworks ensures coverage of known attack vectors.
Key CIS controls to implement:
- Ensure AppArmor/SELinux profiles are enabled — confine container processes.
- Disable privileged containers — privileged mode grants near-full host access.
- Set
--cap-drop=ALLand add only required capabilities — follow least-privilege. - Restrict memory swapping — prevent information leakage via swap.
- Enable container event logging — track container lifecycle for forensic analysis.
# Example: drop all capabilities, add only what's needed
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
myapp
Conclusion
Securing Docker environments is not a one-time task — it's a layered, ongoing discipline that spans image construction, registry governance, runtime enforcement, and continuous monitoring. The most effective strategy combines defense in depth with automation: scan images in CI/CD, enforce policies at the daemon and cluster level, run workloads as non-root with minimal privileges, and monitor for anomalies in real time.
Start with the highest-impact practices — **minimal base images, non-root users, and