A hands-on introduction to containerization with Docker and DevPod.
| Section | Time | Description |
|---|---|---|
| 1. What is a Container? | 5 min | Concepts and terminology |
| 2. Essential Commands | 10 min | Run, stop, inspect containers |
| 3. Writing a Dockerfile | 10 min | Build your own images |
| 4. Hands-on: Flask App | 15 min | Build and run a web app |
| 5. DevPod & Dev Containers | 15 min | Containerized dev environments |
| 6. Useful Extras | 5 min | Volumes, env vars, cleanup |
| Section | Description |
|---|---|
| 7. Dockerfile Best Practices | Layer caching, multi-stage builds |
| 8. Docker Compose | Multi-container applications |
| 9. Networking | Container networks, service discovery |
- Docker installed (Install Docker)
- DevPod installed (
brew install devpodor devpod.sh)
docker --version
docker run hello-worldA container packages code + runtime + dependencies into a single unit that runs anywhere.
| Containers | Virtual Machines |
|---|---|
| Share host OS kernel | Run full OS |
| Lightweight (MBs) | Heavy (GBs) |
| Start in seconds | Start in minutes |
Key concepts:
- Image: Blueprint/snapshot (read-only)
- Container: Running instance of an image
- Dockerfile: Instructions to build an image
- Registry: Image repository (Docker Hub, GitHub Container Registry)
# Pull and run an image
docker pull python:3.12
docker run python:3.12 python --version
# Run interactively
docker run -it python:3.12 bash
# Run with port mapping (host:container)
docker run -p 8080:80 nginx
# Run in background
docker run -d --name mynginx nginx
# List containers
docker ps # running
docker ps -a # all
# Manage containers
docker stop mynginx
docker rm mynginx
docker logs mynginx
# Shell into running container
docker exec -it mynginx bashFROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]Build and run:
docker build -t myapp .
docker run -p 5000:5000 myappTip: Copy requirements first for better caching—dependencies change less often than code.
.dockerignore (exclude from build):
.git
__pycache__
.env
.venv
Build and run the Flask app in example-app/:
cd example-app
docker build -t flask-app .
docker run -p 5000:5000 flask-appVisit http://localhost:5000
DevPod runs containerized dev environments on any infrastructure—local Docker, SSH servers, or cloud.
- Consistent environments across the team
- No "works on my machine" problems
- Quick onboarding—just run
devpod up
myproject/
├── .devcontainer/
│ ├── devcontainer.json
│ └── Dockerfile
└── src/
{
"name": "Python Dev",
"build": { "dockerfile": "Dockerfile" },
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": ["ms-python.python", "charliermarsh.ruff"]
}
},
"postCreateCommand": "pip install -r requirements.txt",
"forwardPorts": [5000]
}# Start dev environment (opens in VS Code)
devpod up ./example-devpod --ide vscode
# From a git repo
devpod up github.com/user/repo --ide vscode
# List workspaces
devpod list
# SSH into workspace
devpod ssh example-devpod
# Stop/delete
devpod stop example-devpod
devpod delete example-devpod# Add SSH provider
devpod provider add ssh
# Run on remote machine
devpod up ./my-project --provider sshdevpod up ./example-devpod --ide vscodedocker run -v $(pwd)/data:/app/data myappdocker run -e API_KEY=secret myapp
docker run --env-file .env myappdocker system prune -a # remove unused images/containersDocker caches each layer. Order instructions from least to most frequently changing:
# Good: dependencies change less often than code
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# Bad: code changes invalidate dependency cache
COPY . .
RUN pip install -r requirements.txtReduce final image size by using build stages:
# Build stage
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
# Production stage
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]# Good: pinned version
FROM python:3.12.1-slim
# Bad: can change unexpectedly
FROM python:latestFor multi-container applications, define services in docker-compose.yml:
services:
web:
build: .
ports:
- "8000:8000"
volumes:
- .:/app
depends_on:
- db
environment:
- DATABASE_URL=postgres://db:5432/mydb
db:
image: postgres:15
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_DB=mydb
- POSTGRES_PASSWORD=secret
volumes:
pgdata:docker compose up # start services
docker compose up -d # start in background
docker compose down # stop services
docker compose logs -f # view logs
docker compose down -v # stop and remove volumesFor projects needing multiple services (app + database):
{
"name": "Python + Postgres",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"forwardPorts": [5000, 5432]
}# Map host port 8080 to container port 80
docker run -p 8080:80 nginx
# Map to specific interface
docker run -p 127.0.0.1:8080:80 nginxContainers on the same network can reach each other by name:
# Create a network
docker network create mynetwork
# Run containers on the same network
docker run -d --name db --network mynetwork postgres
docker run -d --name web --network mynetwork myapp
# From 'web' container, connect to: postgres://db:5432docker network ls
docker network inspect mynetwork| Command | Description |
|---|---|
docker build -t name . |
Build image |
docker run -p 8080:80 image |
Run with port mapping |
docker run -it image bash |
Run interactively |
docker run -d --name x image |
Run in background |
docker ps |
List running containers |
docker stop/rm name |
Stop/remove container |
docker logs name |
View logs |
docker exec -it name bash |
Shell into container |
docker compose up -d |
Start compose services |
docker compose down |
Stop compose services |
devpod up ./project --ide vscode |
Start dev container |
devpod list |
List workspaces |
devpod ssh name |
SSH into workspace |