Docker Image Hardening
A typical production Docker image runs as root and carries hundreds of CVEs. Using only standard Dockerfile techniques, you'll shrink it from 921 MB to under 60 MB, eliminate root, and lock down the filesystem.
The Insecure Image
The Problem with "Works on My Machine" Images
A typical Docker image pulled from a tutorial is nearly 1 GB, runs as root, and carries hundreds of unpatched CVEs. Let's build one and prove it.
Create the app
mkdir -p ~/demo-01 && cd ~/demo-01
cat > app.py << 'EOF'
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"Hello from DevSecOps Demo!")
def log_message(self, format, *args):
pass
HTTPServer(("", 8080), Handler).serve_forever()
EOF
Write the insecure Dockerfile
cat > Dockerfile.insecure << 'EOF'
FROM python:3.8
COPY app.py .
RUN apt-get update && apt-get install -y curl vim
EXPOSE 8080
CMD ["python", "app.py"]
EOF
Build it
docker build -f Dockerfile.insecure -t demo-app:insecure .
Check the damage
docker images demo-app:insecure
docker run --rm demo-app:insecure whoami
Expected:
REPOSITORY TAG SIZE
demo-app insecure ~900MB
root
Root user. Nearly 1 GB. A compromised process has full container privileges.
The Hardened Image
Three Changes, Dramatic Difference
The hardened Dockerfile makes three key decisions:
- Alpine base — 5 MB instead of 900 MB, far fewer CVEs
- Multi-stage build — build tools never reach the final image
- Non-root user — a dedicated
appuserwith no special privileges
Write the hardened Dockerfile
cat > Dockerfile.hardened << 'EOF'
FROM python:3.8-alpine AS builder
WORKDIR /app
COPY app.py .
FROM python:3.8-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/app.py .
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
CMD ["python", "app.py"]
EOF
docker build -f Dockerfile.hardened -t demo-app:hardened .
Compare
docker images demo-app
docker run --rm demo-app:hardened whoami
Expected:
REPOSITORY TAG SIZE
demo-app hardened ~49MB
demo-app insecure ~900MB
appuser
Apply runtime hardening flags
docker run -d \
--name demo-hardened \
--read-only \
--cap-drop=ALL \
--security-opt no-new-privileges:true \
--tmpfs /tmp \
-p 8888:8080 \
demo-app:hardened
curl http://localhost:8888
Expected: Hello from DevSecOps Demo!
Prove the read-only filesystem
docker exec demo-hardened sh -c "echo test > /app/test.txt" 2>&1
Expected: sh: can't create /app/test.txt: Read-only file system
Cleanup
docker stop demo-hardened && docker rm demo-hardened
What You Achieved
| Technique | Insecure | Hardened |
|---|---|---|
| Base image | python:3.8 Debian (~900 MB) | python:3.8-alpine (~49 MB) |
| User | root (UID 0) | appuser (non-root) |
| Build tools in runtime | Yes (curl, vim) | No (multi-stage) |
| Filesystem | Read-write | --read-only |
| Linux capabilities | All inherited | --cap-drop=ALL |
- Next lesson
- Trivy: CVE Scanning & SBOM