🐳 Week 4: Containers & Docker

Day 5: Docker Best Practices

⏱ Duration: 5 Hours

📚 Learning Objectives

  • Master multi-stage builds for smaller images
  • Implement Docker security best practices
  • Push and manage images on Docker Hub
  • Optimize Dockerfiles for production
  • Use .dockerignore effectively

📖 Core Concepts (2 Hours)

Multi-Stage Builds

Multi-stage builds let you use multiple FROM statements. Copy only what you need from build stages to keep final images small and secure.

# Single stage (BAD - huge image with build tools) FROM node:18 WORKDIR /app COPY . . RUN npm install && npm run build CMD ["node", "dist/index.js"] # Result: ~1GB image with dev dependencies # Multi-stage (GOOD - tiny production image) # Stage 1: Build FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # Stage 2: Production FROM node:18-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules CMD ["node", "dist/index.js"] # Result: ~150MB image, no dev tools

Multi-Stage for Compiled Languages

# Go application - extreme size reduction # Stage 1: Build FROM golang:1.21 AS builder WORKDIR /app COPY go.* ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o main . # Stage 2: Minimal runtime FROM alpine:3.18 RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /app/main . CMD ["./main"] # From 1GB+ to ~15MB! # Or even smaller with scratch FROM scratch COPY --from=builder /app/main /main CMD ["/main"] # Just the binary, ~10MB

Docker Security Best Practices

Security Checklist: ┌────────────────────────────────────────────────────┐ │ 1. Don't run as root │ │ 2. Use official/verified base images │ │ 3. Scan images for vulnerabilities │ │ 4. Don't store secrets in images │ │ 5. Use specific image tags, not :latest │ │ 6. Minimize installed packages │ │ 7. Use read-only file systems when possible │ │ 8. Set resource limits │ └────────────────────────────────────────────────────┘

Running as Non-Root User

# BAD - running as root (default) FROM node:18-alpine WORKDIR /app COPY . . CMD ["node", "index.js"] # GOOD - create and use non-root user FROM node:18-alpine WORKDIR /app # Create non-root user RUN addgroup -g 1001 -S appgroup && \ adduser -u 1001 -S appuser -G appgroup COPY --chown=appuser:appgroup . . # Switch to non-root user USER appuser CMD ["node", "index.js"] # Verify docker run myapp whoami # Should print: appuser

Image Scanning & Vulnerabilities

# Scan image with Docker Scout (built-in) docker scout cves myimage:latest docker scout quickview myimage:latest # Use Trivy (popular open-source scanner) # Install: https://aquasecurity.github.io/trivy trivy image myimage:latest trivy image --severity HIGH,CRITICAL myimage:latest # Scan during CI/CD # GitHub Actions example: # - name: Scan image # uses: aquasecurity/trivy-action@master # with: # image-ref: 'myimage:${{ github.sha }}' # severity: 'HIGH,CRITICAL' # Use minimal base images FROM alpine:3.18 # ~5MB, fewer vulnerabilities FROM gcr.io/distroless/static # No shell, minimal attack surface

Docker Hub & Registry

# Login to Docker Hub docker login # Enter username and password/token # Tag image for Docker Hub docker tag myapp username/myapp:v1.0 docker tag myapp username/myapp:latest # Push to Docker Hub docker push username/myapp:v1.0 docker push username/myapp:latest # Pull from Docker Hub docker pull username/myapp:v1.0 # Use in docker-compose.yml services: app: image: username/myapp:v1.0 # Automated builds (link GitHub repo to Docker Hub) # On push → Docker Hub builds and pushes new image # Private registries docker login registry.example.com docker tag myapp registry.example.com/myapp:v1.0 docker push registry.example.com/myapp:v1.0

.dockerignore File

# .dockerignore - exclude files from build context # Version control .git .gitignore # Dependencies (will be installed in container) node_modules vendor __pycache__ # Build outputs dist build *.pyc # Environment and secrets .env .env.* *.pem *.key # IDE and editor files .idea .vscode *.swp # Documentation README.md docs/ # Tests tests/ *.test.js coverage/ # Docker files (not needed in image) Dockerfile* docker-compose* .dockerignore

Dockerfile Optimization Tips

# 1. Order instructions by change frequency # Least changing first, most changing last FROM python:3.11-slim WORKDIR /app COPY requirements.txt . # Changes rarely RUN pip install -r requirements.txt COPY . . # Changes often # 2. Combine RUN commands to reduce layers # BAD RUN apt-get update RUN apt-get install -y curl RUN apt-get install -y git RUN rm -rf /var/lib/apt/lists/* # GOOD RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ git && \ rm -rf /var/lib/apt/lists/* # 3. Use specific versions FROM python:3.11.4-slim # Not python:latest # 4. Clean up in same layer RUN pip install --no-cache-dir -r requirements.txt # 5. Use COPY instead of ADD (unless extracting) COPY local-file.txt /app/ # Predictable ADD archive.tar.gz /app/ # Only when extracting

🔬 Hands-on Lab (2.5 Hours)

Lab 1: Multi-Stage Build

  • Create a multi-stage Dockerfile
  • Compare single vs multi-stage image sizes
  • Build production-ready images
# Lab 1: Multi-stage Node.js app mkdir multi-stage && cd multi-stage # TypeScript app source cat > package.json << 'EOF' { "name": "multi-stage-demo", "scripts": { "build": "tsc", "start": "node dist/index.js" }, "dependencies": { "express": "^4.18.2" }, "devDependencies": { "typescript": "^5.0.0", "@types/express": "^4.17.0", "@types/node": "^20.0.0" } } EOF cat > tsconfig.json << 'EOF' { "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "strict": true }, "include": ["src/**/*"] } EOF mkdir src cat > src/index.ts << 'EOF' import express from 'express'; const app = express(); app.get('/', (req, res) => res.json({ message: 'Multi-stage build!' })); app.listen(3000, () => console.log('Server running')); EOF # Single-stage Dockerfile (for comparison) cat > Dockerfile.single << 'EOF' FROM node:18 WORKDIR /app COPY . . RUN npm install RUN npm run build CMD ["npm", "start"] EOF # Multi-stage Dockerfile cat > Dockerfile << 'EOF' # Build stage FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # Production stage FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY --from=builder /app/dist ./dist # Security: non-root user RUN addgroup -g 1001 -S nodejs && \ adduser -u 1001 -S nodejs -G nodejs USER nodejs EXPOSE 3000 CMD ["npm", "start"] EOF # Build both and compare docker build -f Dockerfile.single -t app:single . docker build -f Dockerfile -t app:multi . # Compare sizes docker images | grep app # app:single ~1GB # app:multi ~170MB (83% smaller!) # Run multi-stage version docker run -d -p 3000:3000 --name demo app:multi curl http://localhost:3000 # Cleanup docker rm -f demo cd ..

Lab 2: Security Hardening

  • Create secure Dockerfile with non-root user
  • Scan for vulnerabilities
  • Apply security best practices
# Lab 2: Security-hardened container mkdir secure-app && cd secure-app cat > app.py << 'EOF' from flask import Flask import os app = Flask(__name__) @app.route('/') def hello(): user = os.popen('whoami').read().strip() return f'Running as: {user}' if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) EOF echo "flask==3.0.0" > requirements.txt # Secure Dockerfile cat > Dockerfile << 'EOF' FROM python:3.11-slim # Security: Create non-root user RUN groupadd -r appgroup && useradd -r -g appgroup appuser WORKDIR /app # Install dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy app files COPY --chown=appuser:appgroup app.py . # Security: Switch to non-root user USER appuser # Security: Don't run as privileged EXPOSE 5000 # Security: Read-only filesystem friendly ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 CMD ["python", "app.py"] EOF # .dockerignore cat > .dockerignore << 'EOF' __pycache__ *.pyc .git .env Dockerfile .dockerignore EOF # Build docker build -t secure-app . # Run with security options docker run -d \ --name secure \ --read-only \ --security-opt=no-new-privileges:true \ --cap-drop=ALL \ -p 5000:5000 \ secure-app # Test curl http://localhost:5000 # Should show: Running as: appuser # Verify security docker exec secure whoami # appuser, not root # Scan for vulnerabilities docker scout cves secure-app 2>/dev/null || echo "Docker Scout not available" # Cleanup docker rm -f secure cd ..

Lab 3: Push to Docker Hub

  • Create Docker Hub account (if needed)
  • Tag and push images
  • Pull and run from Docker Hub
# Lab 3: Docker Hub workflow # Replace YOUR_USERNAME with your Docker Hub username # Login docker login # Create a simple app mkdir dockerhub-demo && cd dockerhub-demo cat > Dockerfile << 'EOF' FROM alpine:3.18 RUN echo "Hello from Docker Hub!" > /message.txt CMD ["cat", "/message.txt"] EOF # Build with your username docker build -t YOUR_USERNAME/hello-demo:v1.0 . docker build -t YOUR_USERNAME/hello-demo:latest . # Test locally docker run YOUR_USERNAME/hello-demo:v1.0 # Push to Docker Hub docker push YOUR_USERNAME/hello-demo:v1.0 docker push YOUR_USERNAME/hello-demo:latest # Remove local images docker rmi YOUR_USERNAME/hello-demo:v1.0 docker rmi YOUR_USERNAME/hello-demo:latest # Pull from Docker Hub docker pull YOUR_USERNAME/hello-demo:v1.0 docker run YOUR_USERNAME/hello-demo:v1.0 # View on Docker Hub: https://hub.docker.com/r/YOUR_USERNAME/hello-demo # Cleanup cd .. rm -rf dockerhub-demo

Lab 4: Production-Ready Compose Setup

  • Apply all best practices in a compose project
  • Health checks, resource limits, security
# Lab 4: Production-ready docker-compose mkdir prod-ready && cd prod-ready # Create optimized Dockerfile mkdir app cat > app/Dockerfile << 'EOF' FROM python:3.11-slim AS base # Create non-root user RUN groupadd -r app && useradd -r -g app app FROM base AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt FROM base AS production WORKDIR /app COPY --from=builder /app/deps /app/deps COPY --chown=app:app app.py . ENV PYTHONPATH=/app/deps USER app EXPOSE 5000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" CMD ["python", "app.py"] EOF cat > app/requirements.txt << 'EOF' flask==3.0.0 gunicorn==21.0.0 EOF cat > app/app.py << 'EOF' from flask import Flask, jsonify app = Flask(__name__) @app.route('/') def index(): return jsonify(status='ok', message='Production ready!') @app.route('/health') def health(): return jsonify(status='healthy') EOF # Production docker-compose.yml cat > docker-compose.yml << 'EOF' version: '3.8' services: app: build: context: ./app target: production restart: unless-stopped ports: - "5000:5000" environment: - FLASK_ENV=production deploy: resources: limits: cpus: '0.5' memory: 256M reservations: memory: 128M healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"] interval: 30s timeout: 10s retries: 3 start_period: 10s security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp EOF # Build and run docker compose up -d --build # Check health docker compose ps sleep 10 curl http://localhost:5000 curl http://localhost:5000/health # View resource usage docker stats --no-stream # Cleanup docker compose down cd .. rm -rf prod-ready multi-stage secure-app

✅ Day 5 Checklist

  • Can write multi-stage Dockerfiles
  • Understand image size optimization
  • Run containers as non-root user
  • Know how to scan for vulnerabilities
  • Can push/pull from Docker Hub
  • Use .dockerignore effectively
  • Apply security best practices

🎯 Week 4 Summary

This week you learned:

  • Day 1: Container basics, Docker installation, running containers
  • Day 2: Docker images, Dockerfiles, FROM/COPY/RUN/CMD
  • Day 3: Volumes, networking, logs, debugging containers
  • Day 4: Docker Compose, multi-container applications
  • Day 5: Multi-stage builds, security, Docker Hub

You're now ready to containerize applications and deploy them anywhere!