Containers revolutionized application deployment, but they also introduced new security challenges. A misconfigured Docker container can expose your entire infrastructure to attack.
Docker Image Security
1. Use Official Base Images
# ❌ Bad: Unknown/untrusted image
FROM random-user/ubuntu:latest
# ✅ Good: Official minimal image
FROM node:18-alpine
2. Specify Exact Versions
# ❌ Bad: Latest tag (unpredictable updates)
FROM node:latest
# ✅ Good: Pinned version with digest
FROM node:18.19.0-alpine3.19@sha256:...
3. Use Minimal Images
Smaller images = smaller attack surface.
# Standard Ubuntu: 77MB
FROM ubuntu:22.04
# Alpine: 5.5MB (14x smaller!)
FROM alpine:3.19
# Distroless: Even smaller, no shell
FROM gcr.io/distroless/nodejs:18
4. Multi-Stage Builds
# ❌ Bad: Build tools in production image
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install # Includes devDependencies
COPY . .
CMD ["node", "server.js"]
# ✅ Good: Build dependencies excluded from final image
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app .
USER node # Non-root user
CMD ["node", "server.js"]
5. Scan for Vulnerabilities
# Scan with Trivy
trivy image your-image:tag
# Scan with Docker Scout
docker scout cves your-image:tag
# Scan with Snyk
snyk container test your-image:tag
Runtime Security
1. Run as Non-Root
# ❌ Bad: Runs as root (UID 0)
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["node", "server.js"]
# ✅ Good: Runs as unprivileged user
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
COPY --chown=nodejs:nodejs . .
USER nodejs
CMD ["node", "server.js"]
2. Read-Only Root Filesystem
docker run --read-only \
--tmpfs /tmp \
your-image:tag
3. Drop Unnecessary Capabilities
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
your-image:tag
4. Limit Resources
docker run \
--memory=512m \
--cpus=0.5 \
--pids-limit=100 \
your-image:tag
Secrets Management
❌ Never Hardcode Secrets
# ❌ NEVER DO THIS
ENV DB_PASSWORD=supersecret123
ENV API_KEY=abc123def456
✅ Use Docker Secrets or Environment Variables
# Docker Swarm secrets
docker service create \
--secret db_password \
your-service
# Kubernetes secrets
kubectl create secret generic db-credentials \
--from-literal=password=supersecret
✅ Use .dockerignore
# .dockerignore
.env
.git
.github
node_modules
*.log
.DS_Store
secrets/
Network Security
1. Use Custom Networks
# Create isolated network
docker network create --driver bridge app-network
# Run containers on custom network
docker run --network=app-network app-container
docker run --network=app-network db-container
2. Limit Port Exposure
# ❌ Bad: Binds to all interfaces
docker run -p 3000:3000 app
# ✅ Good: Binds to localhost only
docker run -p 127.0.0.1:3000:3000 app
Kubernetes Security
1. Pod Security Standards
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: your-app:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
memory: "512Mi"
cpu: "500m"
2. Network Policies
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-network-policy
spec:
podSelector:
matchLabels:
app: api
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
3. RBAC
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"] # Read-only
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
subjects:
- kind: ServiceAccount
name: app-service-account
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
Supply Chain Security
1. Sign Your Images
# Sign with Docker Content Trust
export DOCKER_CONTENT_TRUST=1
docker push your-image:tag
# Verify signature
docker trust inspect your-image:tag
2. Use Private Registries
# Use AWS ECR, Google Artifact Registry, or Harbor
docker tag your-app:latest your-registry.io/your-app:1.0
docker push your-registry.io/your-app:1.0
3. Scan on Build and Deploy
# GitHub Actions example
name: Build and Scan
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t app:$GITHUB_SHA .
- name: Scan image
run: |
docker run aquasec/trivy image app:$GITHUB_SHA
# Fail if high/critical vulnerabilities found
- name: Push to registry
if: success()
run: docker push registry/app:$GITHUB_SHA
Logging & Monitoring
# Enable Docker logging
docker run \
--log-driver=json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
your-app
# Monitor container metrics
docker stats
# Audit Docker daemon events
docker events --filter 'event=start'
Security Checklist
- ✅ Use official, minimal base images
- ✅ Pin image versions with digests
- ✅ Multi-stage builds to reduce image size
- ✅ Run containers as non-root user
- ✅ Read-only root filesystem where possible
- ✅ Drop all capabilities, add only needed ones
- ✅ Limit CPU/memory resources
- ✅ No secrets in images or environment variables
- ✅ Use .dockerignore to exclude sensitive files
- ✅ Custom networks, limit port exposure
- ✅ Regular vulnerability scanning
- ✅ Image signing and verification
- ✅ Network policies in Kubernetes
- ✅ RBAC for least-privilege access
Conclusion
Container security is a multi-layered challenge. By following these best practices, you significantly reduce your attack surface and protect your infrastructure.
Need a comprehensive security audit of your Dockerfiles and Kubernetes configs? Get a Docker/K8s audit and receive detailed security recommendations within 24 hours.