⏱ 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!