CRITICAL 2024: According to the 2024 Docker Security Report, vulnerabilities in container images increased by 25% year-over-year. Security is not optional!
- Overview
- Security Principles
- LAB-11.1: Using Distroless Images
- LAB-11.2: Running as Non-Root User
- LAB-11.3: Vulnerability Scanning with Docker Scout
- LAB-11.4: Vulnerability Scanning with Trivy
- LAB-11.5: Multi-Stage Builds for Security
- LAB-11.6: Using Build Secrets Securely
- LAB-11.7: Security Hardening at Runtime
- Security Checklist
Docker container security involves multiple layers:
- Image Security: Use minimal, secure base images
- Build Security: Don't include secrets in images
- Runtime Security: Run with least privileges
- Scanning: Regularly scan for vulnerabilities
- Updates: Keep images updated with security patches
- Principle of Least Privilege: Run with minimal permissions
- Defense in Depth: Multiple security layers
- Minimal Attack Surface: Smaller images = fewer vulnerabilities
- Immutability: Don't modify running containers
- Regular Scanning: Continuously check for vulnerabilities
Distroless images contain only your application and runtime dependencies—no shell, no package managers, no unnecessary tools.
- 90%+ reduction in potential vulnerabilities
- 10-50MB instead of 100-500MB
- No shell means attackers can't execute commands
Create app-traditional.js:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from traditional image!\n');
});
server.listen(3000);
console.log('Server running on port 3000');Create Dockerfile.traditional:
FROM node:20
WORKDIR /app
COPY app-traditional.js .
EXPOSE 3000
CMD ["node", "app-traditional.js"]Build and check size:
docker build -f Dockerfile.traditional -t app-traditional .
docker images app-traditionalYou'll see the image is ~1GB!
Create Dockerfile.distroless:
FROM node:20 AS builder
WORKDIR /app
COPY app-traditional.js .
# Final stage - Distroless
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app/app-traditional.js /app/app-traditional.js
WORKDIR /app
EXPOSE 3000
CMD ["app-traditional.js"]Build and compare:
docker build -f Dockerfile.distroless -t app-distroless .
docker images | grep app-The distroless image is ~180MB - much smaller!
# Traditional image
docker run -d -p 3001:3000 --name trad app-traditional
curl http://localhost:3001
# Distroless image
docker run -d -p 3002:3000 --name dist app-distroless
curl http://localhost:3002# Traditional - Shell access (DANGEROUS!)
docker exec -it trad sh
# You get a shell! Attacker can run commands!
exit
# Distroless - No shell (SECURE!)
docker exec -it dist sh
# Error: exec failed: unable to start container process: exec: "sh": executable file not foundResult: Distroless prevents shell access, significantly improving security!
docker stop trad dist
docker rm trad distWARNING: 58% of container images run as root (UID 0). This is a major security risk!
Create Dockerfile.root:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
CMD ["whoami"]Build and run:
docker build -f Dockerfile.root -t test-root .
docker run test-rootOutput: root ❌ (DANGEROUS!)
Create Dockerfile.nonroot:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Create app directory and set ownership
RUN mkdir -p /app && chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
WORKDIR /app
CMD ["whoami"]Build and run:
docker build -f Dockerfile.nonroot -t test-nonroot .
docker run test-nonrootOutput: appuser ✅ (SECURE!)
Create app.py:
from http.server import HTTPServer, SimpleHTTPRequestHandler
import os
class MyHandler(SimpleHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
uid = os.getuid()
message = f"Hello! Running as UID: {uid}\n"
self.wfile.write(message.encode())
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8080), MyHandler)
print('Server started on port 8080...')
server.serve_forever()Create Dockerfile.secure:
FROM python:3.11-slim
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Create and set app directory
WORKDIR /app
COPY --chown=appuser:appuser app.py .
# Switch to non-root user
USER appuser
EXPOSE 8080
CMD ["python", "app.py"]Build and run:
docker build -f Dockerfile.secure -t secure-app .
docker run -d -p 8080:8080 --name secureapp secure-app
curl http://localhost:8080You'll see the UID is not 0 (not root)!
docker stop secureapp
docker rm secureappDocker Scout is built into Docker Desktop and provides comprehensive vulnerability scanning.
# Check if Docker Scout is available
docker scout version
# Enroll (if not already)
docker scout enroll# Pull an older, vulnerable image for testing
docker pull nginx:1.18
# Quick scan
docker scout quickview nginx:1.18
# Detailed CVE report
docker scout cves nginx:1.18You'll see a list of vulnerabilities!
# Get recommendations for fixing vulnerabilities
docker scout recommendations nginx:1.18Docker Scout will suggest updating to a newer version!
# Pull latest nginx
docker pull nginx:latest
# Compare old vs new
docker scout compare nginx:1.18 --to nginx:latestYou'll see the improvement!
# Build a test image
cat > Dockerfile.scanme <<EOF
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y curl openssl
EOF
docker build -f Dockerfile.scanme -t scanme:test .
# Scan it
docker scout cves scanme:test
# Get detailed report
docker scout cves --format sarif --output report.json scanme:testTrivy is an open-source security scanner that's very popular in CI/CD pipelines.
Linux:
# Install Trivy
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivymacOS:
brew install trivyDocker (no installation needed):
docker run aquasec/trivy --version# Scan nginx image
trivy image nginx:1.18
# Show only HIGH and CRITICAL vulnerabilities
trivy image --severity HIGH,CRITICAL nginx:1.18
# Output as JSON
trivy image --format json nginx:1.18 > report.jsonCreate Dockerfile.test:
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y python2.7 curlScan it:
trivy config Dockerfile.testTrivy will warn about using old Ubuntu and Python 2!
# Exit with code 1 if vulnerabilities found
trivy image --exit-code 1 --severity HIGH,CRITICAL nginx:1.18
echo $? # Will be 1 if vulnerabilities foundThis is useful in CI/CD to fail the build if vulnerabilities exist!
Multi-stage builds reduce image size and remove build tools from the final image.
Create app.go:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from secure Go app!\n")
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}Create Dockerfile.singlestage:
FROM golang:1.21
WORKDIR /app
COPY app.go .
RUN go build -o app app.go
EXPOSE 8080
CMD ["./app"]Build and check size:
docker build -f Dockerfile.singlestage -t app-single .
docker images app-singleSize: ~1GB (includes entire Go toolchain!)
Create Dockerfile.multistage:
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY app.go .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app app.go
# Final stage - distroless
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/app /app
EXPOSE 8080
CMD ["/app"]Build and compare:
docker build -f Dockerfile.multistage -t app-multi .
docker images | grep app-Size: ~2MB - 500x smaller!
# Single stage
docker run -d -p 8081:8080 --name single app-single
curl http://localhost:8081
# Multi-stage
docker run -d -p 8082:8080 --name multi app-multi
curl http://localhost:8082Both work, but multi-stage is much more secure!
docker stop single multi
docker rm single multiNEVER include secrets in your Docker images!
# ❌ INSECURE - Don't do this!
FROM ubuntu:22.04
COPY .env /app/.env # Secret in image!
COPY api_key.txt /app/ # Secret in image!These secrets will be in the image layers forever!
Create secret.txt:
echo "my-secret-api-key" > secret.txtCreate Dockerfile.secrets:
# syntax=docker/dockerfile:1.4
FROM alpine:latest
# Use secret during build (not stored in image)
RUN --mount=type=secret,id=mysecret \
cat /run/secrets/mysecret && \
echo "Secret used but not stored!"
CMD ["echo", "App running without secrets in image"]Build with secret:
docker build --secret id=mysecret,src=secret.txt \
-f Dockerfile.secrets -t app-secrets .docker run app-secrets
docker history app-secretsThe secret is NOT in the image history!
FROM alpine:latest
# Don't COPY secrets
# Use environment variables at runtime
CMD sh -c 'echo "API Key: $API_KEY"'docker run -e API_KEY=secret-from-runtime myapp# Drop all capabilities
docker run --cap-drop=ALL nginx
# Read-only root filesystem
docker run --read-only nginx
# No new privileges
docker run --security-opt=no-new-privileges nginx
# Combined hardening
docker run \
--read-only \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--user 1000:1000 \
nginx# Limit memory
docker run -m 512m nginx
# Limit CPU
docker run --cpus=1 nginx
# Combined
docker run -m 512m --cpus=1 nginxCreate docker-compose.yml:
services:
web:
image: nginx:alpine
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
user: "101:101"
mem_limit: 512m
cpus: 1
ports:
- "8080:80"Run it:
docker compose up -d
docker compose ps
docker compose down- Use minimal base images (Alpine, Distroless)
- Use specific image tags, not
latest - Multi-stage builds to reduce size
- Run as non-root user
- Don't include secrets in images
- Use
.dockerignorefile - Scan images for vulnerabilities
- Update base images regularly
- Minimize installed packages
- Run containers with
--read-onlywhen possible - Drop unnecessary capabilities
- Use
--security-opt=no-new-privileges - Set resource limits (memory, CPU)
- Use secrets management (Docker secrets, Kubernetes secrets)
- Run as non-root user
- Use private networks
- Enable logging and monitoring
- Regular security audits
- Automated vulnerability scanning
- Fail builds on HIGH/CRITICAL CVEs
- Sign images
- Use private registries
- Implement least privilege access
- Audit Docker daemon logs
- Keep Docker updated
You've learned:
- ✅ Using Distroless images (90%+ vulnerability reduction)
- ✅ Running as non-root user
- ✅ Vulnerability scanning (Docker Scout, Trivy)
- ✅ Multi-stage builds for security
- ✅ Secure secret handling
- ✅ Runtime security hardening
Remember: Security is not a one-time task—it's an ongoing process!