Docker Container Security - Production Hardening Guide
Complete guide to securing Docker containers in production. Learn image scanning, runtime security, network isolation, secrets management, and compliance best practices with real-world examples.
Why Container Security Matters
Containers revolutionized application deployment, but their ephemeral nature and shared kernel architecture introduce unique security challenges. A single misconfigured container can expose your entire infrastructure.
The threat landscape:
- 76% of organizations experienced container security incidents in 2024
- Average cost of a container breach: $4.1 million
- 60% of container images contain known vulnerabilities
This guide covers production-ready security patterns that go beyond basic Docker tutorials.
Image Security: Building Trustworthy Containers
Use Minimal Base Images
Bloated images increase attack surface. Every package is a potential vulnerability.
# ❌ BAD: Full OS with unnecessary packages
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
python3 python3-pip curl wget vim nano
✅ GOOD: Minimal distroless image
FROM gcr.io/distroless/python3-debian11
COPY requirements.txt .
COPY app.py .
CMD ["app.py"]
Why distroless?
- No shell (prevents shell injection attacks)
- No package managers (can't install malware at runtime)
- Minimal dependencies (smaller attack surface)
- 50-80% smaller image sizes
Alternative minimal bases:
alpine:3.18- 5MB base, package manager includedscratch- Empty image, add only your binarygcr.io/distroless/*- Language-specific minimal images
Multi-Stage Builds for Security
Separate build-time dependencies from runtime:
# Stage 1: Build with all tools
FROM node:20-alpine AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
Stage 2: Production runtime (no build tools)
FROM gcr.io/distroless/nodejs20-debian11
WORKDIR /app
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/node_modules ./node_modules
CMD ["dist/server.js"]
Benefits:
- Build tools never reach production
- Final image contains only runtime dependencies
- Reduces image size by 70-90%
- Eliminates unnecessary attack vectors
Image Scanning with Trivy
Scan images for vulnerabilities before deployment:
# Install Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
Scan local image
trivy image my-app:latest
Scan with severity filtering (fail on HIGH/CRITICAL)
trivy image --severity HIGH,CRITICAL --exit-code 1 my-app:latest
Scan specific vulnerability types
trivy image --vuln-type os,library my-app:latest
Output to JSON for CI integration
trivy image --format json --output results.json my-app:latest
CI/CD integration example:
# .github/workflows/security-scan.yml
name: Container Security Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t test-image .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: test-image
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail build on vulnerabilities
Content Trust with Docker Notary
Ensure image integrity and provenance:
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1
Push signed images
docker push myregistry.com/myapp:v1.0
Pull verifies signatures automatically
docker pull myregistry.com/myapp:v1.0
View image signatures
docker trust inspect myregistry.com/myapp:v1.0
Why content trust?
- Prevents man-in-the-middle attacks
- Guarantees image hasn't been tampered with
- Verifies publisher identity
- Meets compliance requirements (PCI-DSS, HIPAA)
Runtime Security: Hardening Running Containers
Run as Non-Root User
Never run containers as root. Create a dedicated user:
FROM python:3.11-slim
Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
Set ownership
WORKDIR /app
COPY --chown=appuser:appuser . .
Switch to non-root user
USER appuser
CMD ["python", "app.py"]
Kubernetes enforcement:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
Read-Only Filesystems
Prevent runtime modifications:
# Docker run with read-only root filesystem
docker run --read-only \
--tmpfs /tmp:rw,noexec,nosuid \
--tmpfs /var/run:rw,noexec,nosuid \
myapp:latest
Docker Compose
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid
- /var/run:rw,noexec,nosuid
Why read-only?
- Prevents malware persistence
- Stops unauthorized file modifications
- Forces stateless application design
- Simplifies security audits
Drop Unnecessary Capabilities
Linux capabilities provide fine-grained privilege control:
# Drop all capabilities, add only what's needed
docker run --cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
myapp:latest
Kubernetes security context
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
Common capabilities to avoid:
SYS_ADMIN- Essentially root accessNET_RAW- Packet sniffing/spoofingSYS_PTRACE- Process debuggingDAC_OVERRIDE- Bypass file permissions
Safe capabilities:
NET_BIND_SERVICE- Bind to ports < 1024CHOWN- Change file ownershipSETUID/SETGID- Change user/group IDs
Security Profiles with AppArmor/Seccomp
Restrict syscalls available to containers:
# Use default seccomp profile
docker run --security-opt seccomp=default.json myapp:latest
Custom seccomp profile (whitelist approach)
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "open", "close", "stat"],
"action": "SCMP_ACT_ALLOW"
}
]
}
Apply custom profile
docker run --security-opt seccomp=./custom-seccomp.json myapp:latest
Kubernetes Pod Security Standards:
apiVersion: v1
kind: Pod
metadata:
name: restricted-pod
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:latest
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
Network Security: Isolation and Segmentation
Custom Bridge Networks
Isolate container groups:
# Create isolated networks
docker network create --driver bridge frontend-net
docker network create --driver bridge backend-net
docker network create --driver bridge db-net
Run containers on specific networks
docker run --network=frontend-net nginx:alpine
docker run --network=backend-net --network=db-net api-server:latest
docker run --network=db-net postgres:15-alpine
Network segmentation rules:
- Frontend → Backend only
- Backend → Database only
- Database isolated (no external access)
- Use network policies to enforce
Network Policies in Kubernetes
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-policy
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
Encrypt Inter-Container Communication
Use service meshes for mTLS:
# Istio sidecar injection
apiVersion: v1
kind: Pod
metadata:
name: secure-app
labels:
app: myapp
annotations:
sidecar.istio.io/inject: "true"
spec:
containers:
- name: app
image: myapp:latest
Benefits:
- Automatic TLS certificate rotation
- Encrypted traffic between all services
- Zero-trust networking
- Traffic observability
Secrets Management: Never Hardcode Credentials
Docker Secrets (Swarm Mode)
# Create secret from file
echo "db_password_here" | docker secret create db_password -
Use secret in service
docker service create
--name api
--secret db_password
myapp:latest
Access in container (read-only file)
cat /run/secrets/db_password
Kubernetes Secrets with Encryption at Rest
# Enable encryption at rest (kube-apiserver flag)
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
Encryption config
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}
Use secrets in pods:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: myapp:latest
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
External Secrets Operators
Integrate with vault services:
# External Secrets Operator with AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: app-secret
data:
- secretKey: db-password
remoteRef:
key: prod/db/password
Supported backends:
- AWS Secrets Manager
- HashiCorp Vault
- Azure Key Vault
- Google Secret Manager
- 1Password, Doppler, etc.
Resource Limits: Preventing DoS
CPU and Memory Constraints
# Docker resource limits
docker run -d \
--memory="512m" \
--memory-swap="512m" \
--cpus="0.5" \
--pids-limit=100 \
myapp:latest
Kubernetes resource quotas:
apiVersion: v1
kind: Pod
metadata:
name: resource-limited
spec:
containers:
- name: app
image: myapp:latest
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Prevent Fork Bombs
# Limit number of processes
docker run --pids-limit=200 myapp:latest
Kubernetes
securityContext:
sysctls:
- name: kernel.pid_max
value: "200"
Compliance and Auditing
CIS Docker Benchmark
Automated compliance checking:
# Install Docker Bench for Security
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo sh docker-bench-security.sh
Example output
[PASS] 1.1.1 Ensure a separate partition for containers has been created
[WARN] 1.1.2 Ensure only trusted users are allowed to control Docker daemon
[PASS] 4.1 Ensure that a user for the container has been created
[FAIL] 4.5 Ensure Content trust for Docker is Enabled
Runtime Monitoring with Falco
Detect anomalous container behavior:
# Falco rules for container security
- rule: Write below root
desc: Detect writes below root directory
condition: >
container and evt.type = open and
evt.arg.flags contains O_WRONLY and
fd.name startswith /
output: "Write below root (user=%user.name command=%proc.cmdline file=%fd.name)"
priority: WARNING
- rule: Container run as root
desc: Detect container running as root
condition: container and user.uid = 0
output: "Container running as root (container=%container.name image=%container.image.repository)"
priority: WARNING
Audit Logging
Enable comprehensive logging:
# Docker daemon audit logging
dockerd --log-driver=syslog --log-opt syslog-address=udp://logserver:514
Kubernetes audit policy
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["pods", "secrets"]
Production Security Checklist
Before deploying to production, verify:
Image Security:
- Use minimal base images (distroless/alpine)
- Multi-stage builds implemented
- No secrets in image layers
- Images scanned for vulnerabilities (Trivy/Snyk)
- Content trust enabled
- Images signed and verified
Runtime Security:
- Run as non-root user
- Read-only root filesystem
- Dropped unnecessary capabilities
- Seccomp/AppArmor profiles applied
- Resource limits configured
- PID limits enforced
Network Security:
- Network segmentation implemented
- Network policies enforced
- No host network mode
- mTLS between services
Secrets Management:
- No hardcoded secrets
- External secrets manager integrated
- Secrets encrypted at rest
- Secret rotation implemented
Monitoring:
- Runtime security monitoring (Falco)
- Audit logging enabled
- Anomaly detection configured
- Incident response plan documented
Real-World Example: Secure Microservice
Complete production-ready configuration:
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app
FROM gcr.io/distroless/static-debian11
COPY --from=builder /build/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-api
spec:
replicas: 3
selector:
matchLabels:
app: secure-api
template:
metadata:
labels:
app: secure-api
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: myregistry.com/secure-api:v1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
ports:
- containerPort: 8080
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: db-host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Conclusion
Container security isn't optional—it's foundational to modern infrastructure. The patterns covered here represent industry best practices validated across thousands of production deployments.
Key takeaways:
- Start with minimal images - Less code = fewer vulnerabilities
- Enforce non-root users - Privilege escalation is the #1 attack vector
- Scan continuously - New vulnerabilities emerge daily
- Isolate networks - Assume breach, limit blast radius
- Never hardcode secrets - Use external managers with rotation
- Monitor runtime behavior - Detect anomalies before incidents
Security is a journey, not a destination. Review your configurations quarterly, update dependencies monthly, and scan on every commit.
Next steps:
- Run Docker Bench Security against your infrastructure
- Implement automated vulnerability scanning in CI/CD
- Deploy runtime security monitoring (Falco)
- Document incident response procedures
Resources:
Related Articles
GraphQL API Design - Production Architecture and Best Practices for Scalable Systems
Master GraphQL API design covering schema design principles, resolver optimization, N+1 query prevention with DataLoader, authentication and authorization patterns, caching strategies, error handling, and production deployment for high-performance GraphQL systems.
Testing Strategies - Unit, Integration, and E2E Testing Best Practices for Production Quality
Comprehensive guide to testing strategies covering unit tests, integration tests, end-to-end testing, test-driven development, mocking patterns, testing pyramid, and production testing practices for reliable software delivery.
Monitoring and Observability - Production Systems Performance and Debugging at Scale
Master monitoring and observability covering metrics collection with Prometheus, distributed tracing with OpenTelemetry, log aggregation, alerting strategies, SLOs/SLIs, and production debugging techniques for reliable systems.
Written by StaticBlock Editorial
StaticBlock Editorial is a technical writer and software engineer specializing in web development, performance optimization, and developer tooling.