Docker Security Hardening: Production Checklist
Contents
Context
Most Docker security incidents aren’t hackers discovering 0days—they’re you forgot to disable privileged mode, ran as root, or exposed unnecessary ports.
This is a practical checklist, not theory. Straight to what to check.
Basic Configuration Checks
1. Don’t Run Containers as Root
# ❌ Wrong: running as root inside container
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# ✅ Correct: create non-root user
FROM node:20
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]# Same in Kubernetes
securityContext:
runAsNonRoot: true
runAsUser: 10002. Read-Only Filesystem
# docker-compose.yml
services:
app:
read_only: true
# Use tmpfs for places that need writes
tmpfs:
- /tmp
- /var/runContainer filesystem is read-only—malicious code can’t write.
3. Limit Capabilities
Linux capabilities are root privileges finely divided. Containers don’t need CAP_SYS_ADMIN.
# docker-compose.yml
services:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # only add what you need# Check current container capabilities
docker inspect container_name | grep -i capImage Security
4. Use distroless or alpine
# ❌ Don't use ubuntu/debian (200+MB, larger attack surface)
FROM ubuntu:22.04
# ✅ Use distroless (minimal, only runtime needed)
FROM gcr.io/distroless/static-debian12
# ✅ Or alpine (lightweight, only 5MB)
FROM alpine:3.195. Don’t Cache Dependencies in Build
# ❌ Wrong: COPY everything then install deps
COPY . .
RUN npm install
# ✅ Correct: package.json first, install deps, then code
COPY package.json package-lock.json* ./
RUN npm ci --only=production
COPY . .Benefits:
- Fail fast on dependency issues
- Better Docker build cache utilization
- Smaller image layers
6. Set Health Check
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]Network Isolation
7. Network Minimization
services:
app:
networks:
- backend-network
db:
networks:
- backend-network
# DB doesn't need external exposure
networks:
backend-network:
driver: bridgeApp only exposes API port, database fully internal.
8. Don’t Use Host Network Mode
# ❌ Dangerous: container uses host network directly
network_mode: "host"
# ✅ Correct: default bridge, port mapping when needed
services:
app:
ports:
- "8080:3000"Host mode bypasses Docker’s network isolation—container gets direct access to host network.
Secret Management
9. Don’t Store Secrets in Environment Variables
# ❌ Dangerous: secrets in environment variables
environment:
DATABASE_PASSWORD: "supersecret"
# ✅ Correct: use Docker secrets (Swarm mode)
secrets:
db_password:
file: ./db_password.txt
services:
app:
secrets:
- source: db_password
target: db_password
mode: 0400
# ✅ Or use external secret management (Vault, AWS Secrets Manager)# Kubernetes uses external secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
secretStoreRef:
name: vault-backend
target:
name: app-secrets
data:
- secretKey: db-password
remoteRef:
key: prod/database/passwordRuntime Security
10. Use AppArmor / seccomp to Limit Syscalls
# Default seccomp profile already blocks 44 dangerous syscalls
# But some scenarios need stricter controls
# View default seccomp
docker run --rm \
alpine \
cat /proc/sys/kernel/seccomp/action_on_sigfaultProduction recommends non-privileged containers:
security_opt:
- no-new-privileges:true11. Resource Limits
services:
app:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256MPrevents containers from exhausting host resources.
CI/CD Security Scanning
12. Trivy for Image Vulnerability Scanning
# .github/workflows/ci.yml
- name: Scan image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE_TAG }}
format: 'sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # fail on vulnerabilities# Run locally too
trivy image your-image:tag
# Scan known CVEs
trivy image -- vuln-type=os,library your-image:tag13. Don’t Use :latest
# ❌ :latest means you don't know the actual version
image: myapp:latest
# ✅ Use deterministic version
image: myapp:v2.3.1
image: myapp@sha256:abc123...Summary Checklist
Production Docker must-dos:
□ Run as non-root user
□ Read-only filesystem
□ Minimize capabilities
□ distroless/alpine base image
□ health check configured
□ network isolation
□ no host mode
□ secrets not in env vars
□ resource limits
□ CI/CD vulnerability scanning
□ fixed image versionsRun through this list—prevents most Docker security incidents.