1. Teaching Objectives
By the end of this module, students will be able to: - Understand what containers are and why they're essential for CD - Install Docker and run their first container - Build Docker images for Python applications - Use multi-stage builds to optimize images - Push images to Amazon ECR (Elastic Container Registry) - Integrate Docker into CD pipelines - Apply Docker best practices for production deployments
2. What is Docker? Understanding Containers
2.1 The Problem Docker Solves
Before Docker: - "Works on my machine" - code runs on developer's laptop but fails in production - Different environments (dev, staging, prod) have different configurations - Difficult to reproduce issues - Manual setup of servers, dependencies, configurations - Inconsistent deployments
With Docker: - Same environment everywhere (dev, staging, production) - Package everything together (code + dependencies + runtime) - Easy to reproduce and debug - Consistent, repeatable deployments - Fast startup times
2.2 Containers vs Virtual Machines
Virtual Machines (VMs): - Each VM has its own operating system - Heavy (GBs of disk space) - Slow to start (minutes) - High resource usage
Containers: - Share the host operating system - Lightweight (MBs of disk space) - Fast to start (seconds) - Low resource usage - Isolated from other containers
Analogy: - VM = A complete house (foundation, walls, roof, utilities) - Container = An apartment in a building (shares foundation, utilities, but has its own space)
2.3 Key Docker Concepts
| Term | Definition | Example |
|---|---|---|
| Image | A read-only template for creating containers | python:3.12-slim |
| Container | A running instance of an image | Your app running in a container |
| Dockerfile | Instructions for building an image | Recipe for creating your image |
| Registry | Storage for images (like GitHub for code) | Docker Hub, Amazon ECR |
| Docker Hub | Public registry of Docker images | docker pull python:3.12 |
3. Installing Docker
3.1 Installation Steps
macOS:
# Install Docker Desktop from https://www.docker.com/products/docker-desktop
# Or use Homebrew:
brew install --cask docker
Linux (Ubuntu/Debian):
# Update package index
sudo apt-get update
# Install Docker
sudo apt-get install docker.io
# Start Docker service
sudo systemctl start docker
sudo systemctl enable docker
# Add your user to docker group (to run without sudo)
sudo usermod -aG docker $USER
# Log out and back in for changes to take effect
Windows: - Download Docker Desktop from https://www.docker.com/products/docker-desktop - Install and restart
3.2 Verify Installation
# Check Docker version
docker --version
# Output: Docker version 24.0.0, build ...
# Run a test container
docker run hello-world
# This should download and run a test image
4. Your First Container
4.1 Running a Simple Container
# Run a Python container interactively
docker run -it python:3.12-slim python
# You're now in a Python REPL inside the container!
# Try: print("Hello from Docker!")
# Exit with: exit()
4.2 Understanding Docker Commands
# docker run: Create and start a container
docker run python:3.12-slim python --version
# docker ps: List running containers
docker ps
# docker ps -a: List all containers (including stopped)
docker ps -a
# docker images: List downloaded images
docker images
# docker stop: Stop a running container
docker stop <container-id>
# docker rm: Remove a container
docker rm <container-id>
# docker rmi: Remove an image
docker rmi <image-id>
5. Building Docker Images for Python Apps
5.1 Understanding Dockerfiles
A Dockerfile is a text file with instructions for building a Docker image. Think of it as a recipe.
Basic Dockerfile Structure:
# Step 1: Choose base image
FROM python:3.12-slim
# Step 2: Set working directory
WORKDIR /app
# Step 3: Copy requirements file
COPY requirements.txt .
# Step 4: Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Step 5: Copy application code
COPY app app
# Step 6: Expose port
EXPOSE 8080
# Step 7: Define command to run
CMD ["uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "8080"]
5.2 Dockerfile for Our Sample Python App
Let's examine the Dockerfile for our sample Python application:
# syntax=docker/dockerfile:1.7-labs
# Stage 1: Base image with Python
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Stage 2: Builder stage (for compiling dependencies)
FROM base AS builder
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
COPY requirements.txt requirements-dev.txt ./
RUN pip install --upgrade pip && pip install -r requirements.txt
# Stage 3: Runtime stage (final, minimal image)
FROM base AS runtime
COPY --from=builder /usr/local /usr/local
COPY app app
COPY requirements.txt .
ENV PORT=8080
EXPOSE 8080
CMD ["uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "8080"]
Explanation: - Multi-stage build: Creates smaller final image by using separate stages - Stage 1 (base): Sets up Python environment - Stage 2 (builder): Installs build tools and dependencies - Stage 3 (runtime): Final image with only runtime dependencies (no build tools)
5.3 Building Your First Image
# Navigate to your project directory
cd sample_python_app
# Build the image
docker build -t sample-python-app:latest .
# Explanation:
# -t: Tag (name) the image as "sample-python-app:latest"
# . : Build context (current directory)
# Build with a specific tag (recommended for CD)
docker build -t sample-python-app:v1.0.0 .
docker build -t sample-python-app:$(git rev-parse --short HEAD) .
5.4 Running Your Container
# Run the container
docker run -p 8080:8080 sample-python-app:latest
# Explanation:
# -p 8080:8080: Map port 8080 on host to port 8080 in container
# Format: -p HOST_PORT:CONTAINER_PORT
# Test the application
curl http://localhost:8080/healthz
# Run in detached mode (background)
docker run -d -p 8080:8080 --name my-app sample-python-app:latest
# View logs
docker logs my-app
# Stop the container
docker stop my-app
6. Multi-Stage Builds Explained
6.1 Why Multi-Stage Builds?
Problem with single-stage builds: - Build tools (compilers, build-essential) end up in final image - Larger image size (slower to download, more storage) - Security risk (more attack surface)
Solution: Multi-stage builds - Use one stage for building - Use another stage for runtime - Only copy what's needed to final stage
6.2 Multi-Stage Build Example
# Stage 1: Build stage
FROM python:3.12-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && \
apt-get install -y build-essential gcc && \
rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Runtime stage
FROM python:3.12-slim AS runtime
WORKDIR /app
# Copy only installed packages from builder
COPY --from=builder /root/.local /root/.local
# Copy application code
COPY app app
# Make sure scripts in .local are usable
ENV PATH=/root/.local/bin:$PATH
# Expose port
EXPOSE 8080
# Run application
CMD ["uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "8080"]
Benefits: - Final image is much smaller (no build tools) - Faster to download and deploy - More secure (fewer packages) - Faster container startup
7. Docker Registries and Amazon ECR
7.1 What is a Registry?
A registry is a storage and distribution system for Docker images. Think of it like: - GitHub for code → Docker Registry for images - Public registries: Docker Hub, GitHub Container Registry - Private registries: Amazon ECR, Google Container Registry, Azure Container Registry
7.2 Amazon ECR (Elastic Container Registry)
Why ECR for AWS deployments? - Integrated with AWS services (EKS, ECS, Lambda) - Secure (IAM-based access control) - Fast (same region as your services) - Cost-effective (pay for storage and data transfer)
7.3 Setting Up ECR
Step 1: Create ECR Repository
# Using AWS CLI
aws ecr create-repository \
--repository-name sample-python-app \
--region us-east-1
# Output includes repository URI:
# 123456789012.dkr.ecr.us-east-1.amazonaws.com/sample-python-app
Step 2: Authenticate Docker to ECR
# Get login token and authenticate
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789012.dkr.ecr.us-east-1.amazonaws.com
Step 3: Tag Your Image
# Tag image with ECR repository URI
docker tag sample-python-app:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/sample-python-app:latest
# Tag with version
docker tag sample-python-app:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/sample-python-app:v1.0.0
# Tag with commit SHA (common in CD)
docker tag sample-python-app:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/sample-python-app:abc1234
Step 4: Push Image to ECR
# Push the image
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/sample-python-app:latest
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/sample-python-app:v1.0.0
7.4 Pulling Images from ECR
# Pull image from ECR
docker pull 123456789012.dkr.ecr.us-east-1.amazonaws.com/sample-python-app:v1.0.0
# Run container from ECR image
docker run -p 8080:8080 \
123456789012.dkr.ecr.us-east-1.amazonaws.com/sample-python-app:v1.0.0
8. Docker Best Practices for CD
8.1 Image Tagging Strategy
Good Practices:
# Use semantic versioning
docker build -t my-app:v1.2.3 .
# Use commit SHA for traceability
docker build -t my-app:$(git rev-parse --short HEAD) .
# Use both
docker build -t my-app:v1.2.3-abc1234 .
# Never use only 'latest' in production
# 'latest' can change, making rollbacks difficult
Bad Practices:
# Don't rely only on 'latest'
docker build -t my-app:latest . # ❌ Too vague
# Don't use random tags
docker build -t my-app:test123 . # ❌ Not meaningful
8.2 .dockerignore File
Create a .dockerignore file to exclude unnecessary files from build context:
# .dockerignore
# Exclude Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
# Exclude virtual environments
venv/
env/
.venv
# Exclude Git
.git/
.gitignore
# Exclude IDE files
.vscode/
.idea/
*.swp
# Exclude tests (if not needed in production)
tests/
*.test.py
# Exclude documentation
docs/
README.md
# Exclude CI/CD files
.github/
.gitlab-ci.yml
Benefits: - Faster builds (smaller context) - Smaller images - More secure (no sensitive files)
8.3 Layer Caching
Docker caches layers. Order instructions from least to most frequently changing:
# ✅ Good: Dependencies change less frequently
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt . # Copy requirements first
RUN pip install -r requirements.txt # Install dependencies
COPY app app # Copy code last (changes more often)
# ❌ Bad: Code copied before dependencies
FROM python:3.12-slim
WORKDIR /app
COPY app app # Code changes frequently
COPY requirements.txt .
RUN pip install -r requirements.txt # This runs every time code changes
8.4 Security Best Practices
# ✅ Use specific base image tags (not 'latest')
FROM python:3.12-slim # Good: specific version
# ✅ Run as non-root user
RUN useradd -m -u 1000 appuser
USER appuser
# ✅ Don't store secrets in image
# Use environment variables or secrets management
ENV DATABASE_URL=${DATABASE_URL} # Set at runtime
# ✅ Keep base images updated
# Regularly rebuild to get security patches
FROM python:3.12-slim # Check for updates regularly
# ✅ Use minimal base images
FROM python:3.12-slim # Good: minimal
# FROM python:3.12 # Bad: includes unnecessary tools
8.5 Health Checks
Add health checks to your Dockerfile:
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/healthz || exit 1
# Or use Python
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8080/healthz')" || exit 1
9. Docker Compose for Local Development
Docker Compose helps manage multi-container applications:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- ENV=development
volumes:
- ./app:/app/app # Mount code for hot reload
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
interval: 30s
timeout: 10s
retries: 3
# Example: Add a database
# db:
# image: postgres:15
# environment:
# POSTGRES_DB: myapp
# POSTGRES_USER: user
# POSTGRES_PASSWORD: password
Usage:
# Start services
docker-compose up
# Start in background
docker-compose up -d
# View logs
docker-compose logs -f
# Stop services
docker-compose down
10. Integrating Docker into CD Pipelines
10.1 CD Pipeline with Docker
Here's how Docker fits into a CD pipeline:
# .github/workflows/cd.yml
name: Continuous Deployment
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: sample-python-app
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
10.2 Building Images in CI/CD
Best Practices: 1. Build once, deploy everywhere: Build image once, use same image for all environments 2. Tag with commit SHA: Enables traceability and rollback 3. Scan for vulnerabilities: Add security scanning step 4. Cache layers: Use BuildKit for faster builds
Example with security scanning:
- name: Build Docker image
run: docker build -t my-app:${{ github.sha }} .
- name: Scan image for vulnerabilities
run: |
docker scout cves my-app:${{ github.sha }} || true
# Or use Trivy:
# trivy image my-app:${{ github.sha }}
- name: Push to ECR
run: |
docker push my-app:${{ github.sha }}
11. Troubleshooting Common Issues
Issue 1: "Cannot connect to Docker daemon"
# Solution: Start Docker service
sudo systemctl start docker
# Or check if Docker Desktop is running (macOS/Windows)
Issue 2: "Out of disk space"
# Clean up unused images, containers, volumes
docker system prune -a
# Remove specific unused images
docker image prune -a
Issue 3: "Image build fails"
# Build with verbose output
docker build --progress=plain -t my-app .
# Build without cache to see full process
docker build --no-cache -t my-app .
Issue 4: "Container exits immediately"
# Check logs
docker logs <container-id>
# Run interactively to debug
docker run -it my-app /bin/bash
12. Hands-On Exercises
Exercise 1: Build and Run
- Build the sample Python app image
- Run it locally
- Test the
/healthzendpoint - View container logs
Exercise 2: Multi-Stage Build
- Create a simple single-stage Dockerfile
- Build and check image size
- Convert to multi-stage build
- Compare image sizes
Exercise 3: Push to ECR
- Create ECR repository
- Authenticate Docker
- Build, tag, and push image
- Pull and run image from ECR
13. Key Takeaways
- ✅ Containers package applications with dependencies for consistency
- ✅ Dockerfiles define how to build images
- ✅ Multi-stage builds create smaller, more secure images
- ✅ Registries store and distribute images (ECR for AWS)
- ✅ Tag images with versions/commit SHAs for traceability
- ✅ Use .dockerignore to exclude unnecessary files
- ✅ Order Dockerfile instructions for optimal caching
- ✅ Integrate Docker builds into CD pipelines
14. Next Steps
Now that you understand Docker, you're ready for: - Module: Kubernetes Basics for CD - Deploy containers to Kubernetes - Module: Terraform for Kubernetes CD - Automate infrastructure provisioning - Module: Complete CD Pipeline - Put it all together
15. Instructor Notes
- Start with hands-on Docker commands before diving into Dockerfiles
- Use visual diagrams to explain container concepts
- Emphasize the "build once, run anywhere" benefit
- Show real-world examples of Docker in production
- Encourage students to experiment with Docker commands
- Highlight security best practices early