Container Security¶
Running containers as root is the Docker equivalent of leaving your front door wide open with a neon "VACANCY" sign — and roughly 80% of public Docker images do exactly this because every tutorial ever written ignores security for simplicity Container security isn't optional in production. It's the difference between a bored attacker finding a foothold and them moving on to an easier target
Running as Non-Root — The Single Most Important Thing¶
# Bad — every tutorial on Earth
FROM node:20-slim
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]
# Good — minimal privilege
FROM node:20-slim
RUN groupadd -r appgroup && \
useradd -r -g appgroup -m appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm install --production
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
If your container needs port 80 or 443 (privileged ports under 1024), you either: * Run as root (don't) * Use a reverse proxy like Nginx that handles port binding * Add the CAP_NET_BIND_SERVICE capability instead
# Better — specific capability instead of full root
RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/node
# Or use a reverse proxy on port 80 that forwards to your app on 3000
Read-Only Root Filesystem — Lock Down / After Deploy¶
Your app writes logs and temp files to / — that's a security nightmare if an attacker gains RCE and can modify binaries or plant persistent backdoors
services:
app:
image: myapp
read_only: true
tmpfs:
- /tmp # Writeable temp directory in memory
- /var/run # Writeable runtime directory
volumes:
- app_data:/app/data # Persistent writeable data
With read_only: true, the container filesystem is immutable. Any process trying to write to / (except mounted volumes and tmpfs) gets a permissions error. Attackers can't modify your binaries
Image Scanning — Don't Run Known-Vulnerable Shit¶
Trivy is free , fast , and actually useful. Run it on every image before pushing to production
# Install Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
# Scan an image
trivy image node:20-slim
# Scan with severity filter
trivy image --severity CRITICAL,HIGH myapp:latest
# Scan Dockerfile for misconfigurations
trivy config --severity CRITICAL,HIGH .
# Scan your Compose file
trivy config docker-compose.yml
Docker Scout (built into Docker Desktop):
# Enable Scout
docker scout quickview node:20-slim
# Compare to a baseline
docker scout compare myapp:latest --to node:20-slim
GitHub Actions integration:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:latest'
format: 'sarif'
output: 'trivy-results.sarif'
Secret Management in Docker — You're Doing It Wrong¶
# NEVER do this
ENV DATABASE_URL=postgres://admin:supersecret@db:5432/prod
# This is baked into the image. Anyone who pulls the image sees it.
Build-time secrets (Docker BuildKit):
# docker-compose.yml or Dockerfile
# Syntax: docker build --secret id=mysecret,src=./secret.txt .
RUN --mount=type=secret,id=mysecret \
export MY_SECRET=$(cat /run/secrets/mysecret) && \
echo "Build step that needs secret"
Runtime secrets (Docker Compose):
services:
app:
secrets:
- db_password
environment:
- DATABASE_URL=postgres://user:${DB_PASSWORD}@db:5432/prod
secrets:
db_password:
file: ./secrets/db_password.txt # Never commit this
Avoiding Privilege Escalation¶
services:
app:
image: myapp
# Prevent container from gaining more privileges
security_opt:
- no-new-privileges:true
# Drop all capabilities , add back only what's needed
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if app needs privileged ports
# Prevent privilege escalation vectors
privileged: false # Never set to true in production
Resource Limits — Prevent DoS Against Your Own Infrastructure¶
A memory leak in one container shouldn't take down your entire host
services:
app:
image: myapp
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
# Docker run equivalents
docker run -d --memory="512m" --cpus="0.5" myapp
Why this matters: * No memory limit = container can OOM your entire host * No CPU limit = one CPU-intensive process starves all other containers * OOM kills happen silently — you get a stopped container with exit code 137 * Resource limits in production are non-negotiable — every container needs upper bounds
Docker Bench Security — Automated Security Audit¶
# Run the CIS Docker Benchmark
docker run --rm \
--net host \
--pid host \
--userns host \
--cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /etc:/etc \
-v /usr/bin/containerd:/usr/bin/containerd \
-v /usr/bin/runc:/usr/bin/runc \
-v /usr/lib/systemd:/usr/lib/systemd \
-v /var/lib:/var/lib \
-v /var/run/docker.sock:/var/run/docker.sock \
docker/docker-bench-security
Prerequisites¶
Dockerfile section , Docker Compose section
next → devops_05_ci_intro.md