🐳 Docker
Docker — Containers from Zero
Build, ship, and run any application inside a lightweight, portable container. Docker is the foundation of all modern DevOps.
⚙️ How Docker Works
Docker Architecture
Docker CLI ──────────────────────────► Docker Daemon (dockerd)
(your terminal) (background service)
│
┌────────────┼────────────┐
▼ ▼ ▼
Containers Images Volumes
(running processes) (blueprints) (persistent data)
Image Layers (read-only, cached, shared):
┌──────────────────────────────┐
│ Layer 4: COPY app.py /app │ ← your code
│ Layer 3: RUN pip install … │ ← dependencies
│ Layer 2: RUN apt-get … │ ← system packages
│ Layer 1: FROM python:3.12 │ ← base image
└──────────────────────────────┘
+
┌──────────────────────────────┐
│ Writable container layer │ ← data written at runtime (lost on rm)
└──────────────────────────────┘
📦 Images
An image is a read-only blueprint. Pull official images from Docker Hub, or build your own.
# Pull an image
docker pull nginx:1.25
docker pull python:3.12-slim
docker pull postgres:16-alpine
# List local images
docker images
# Inspect image layers
docker image history python:3.12-slim
# Remove image
docker rmi nginx:1.25
# Remove all unused images
docker image prune -a
# Tag image (required before pushing)
docker tag myapp:latest myregistry.com/myteam/myapp:1.0
# Push to registry
docker push myregistry.com/myteam/myapp:1.0
| Base Image | Size | Best For |
|---|---|---|
alpine:3.19 | ~7 MB | Smallest possible — shell scripts, tiny tools |
debian:bookworm-slim | ~75 MB | Good default, familiar package manager |
python:3.12-slim | ~130 MB | Python apps — much smaller than the full image |
node:20-alpine | ~140 MB | Node.js apps |
golang:1.22-alpine | ~240 MB | Go build stage only — use multi-stage builds |
nginx:alpine | ~45 MB | Static file serving, reverse proxy |
🏃 Containers
# Run in background (-d), map port (-p), give a name (--name)
docker run -d -p 8080:80 --name web nginx
# Run interactively and delete when done
docker run -it --rm ubuntu bash
# Pass environment variables
docker run -d \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=mydb \
-p 5432:5432 \
--name db postgres:16
# List running containers
docker ps
# List ALL containers (including stopped)
docker ps -a
# Stream logs
docker logs -f web
# Open a shell inside a running container
docker exec -it web bash
docker exec -it web sh # alpine images don't have bash
# Copy files between host and container
docker cp web:/etc/nginx/nginx.conf ./nginx.conf
docker cp ./nginx.conf web:/etc/nginx/nginx.conf
# Live resource stats
docker stats
# Stop and remove
docker stop web && docker rm web
docker rm -f web # force-stop and remove in one command
| Flag | Meaning | Example |
|---|---|---|
-d | Detach (run in background) | docker run -d nginx |
-it | Interactive + terminal | docker run -it ubuntu bash |
-p host:container | Publish port | -p 8080:80 |
--name | Container name | --name myapp |
--rm | Auto-delete on stop | --rm |
-e KEY=VALUE | Environment variable | -e DB_HOST=db |
-v | Volume / bind mount | -v /data:/app/data |
--network | Attach to network | --network mynet |
--restart | Restart policy | --restart unless-stopped |
--memory | Memory limit | --memory 512m |
--cpus | CPU limit | --cpus 1.5 |
📝 Dockerfile
A Dockerfile is the recipe for building your image. Each instruction adds a layer.
# ── Stage 1: Build ──────────────────────────────────────
FROM python:3.12-slim
# Set working directory inside the container
WORKDIR /app
# Copy requirements FIRST so pip install is cached
# This layer only rebuilds when requirements.txt changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Now copy the rest of the code
COPY . .
# Create a non-root user (security best practice)
RUN useradd -r -u 1001 appuser && chown -R appuser /app
USER appuser
# Document which port the app uses
EXPOSE 8080
# Health check — Docker marks container unhealthy if this fails
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Default command (exec form — correct signal handling)
CMD ["python", "app.py"]
Multi-Stage Build (keep images tiny)
# Stage 1: compile Go binary
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .
# Stage 2: tiny final image — no Go toolchain included
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/server /usr/local/bin/server
EXPOSE 8080
CMD ["server"]
# Result: ~12 MB instead of ~350 MB
.git
node_modules
__pycache__
*.pyc
.env
*.log
dist/
coverage/
Cache tip
Put instructions that change rarely (FROM, RUN apt install) at the top. Put code COPY at the bottom. Changing one layer invalidates all layers below it.
💾 Volumes
# Named volume (Docker manages location)
docker volume create pgdata
docker run -d -v pgdata:/var/lib/postgresql/data postgres:16
# Bind mount (your folder → container path)
docker run -d -v /home/alice/data:/app/data myapp
docker run -d -v $(pwd):/app myapp # current directory
# Read-only bind mount
docker run -d -v ./config:/app/config:ro myapp
# Volume commands
docker volume ls
docker volume inspect pgdata
docker volume rm pgdata
docker volume prune # remove all unused volumes
# Backup a volume
docker run --rm \
-v pgdata:/data -v $(pwd):/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .
# Restore
docker run --rm \
-v pgdata:/data -v $(pwd):/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /data
🌐 Networking
# Create a custom network (enables DNS by container name)
docker network create mynet
# Both containers can reach each other by name
docker run -d --name db --network mynet postgres:16
docker run -d --name api --network mynet myapi:latest
docker run -d --name cache --network mynet redis:7-alpine
# Inside "api" container, DNS works:
# curl http://db:5432 → reaches postgres
# ping cache → reaches redis
# Network commands
docker network ls
docker network inspect mynet
docker network connect mynet existing-container
docker network rm mynet
📋 Docker Compose
Define your entire multi-container application in one YAML file.
# docker-compose.yml
services:
web:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
volumes:
- .:/app # live code in development
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- redis-data:/data
command: redis-server --appendonly yes
volumes:
pg-data:
redis-data:
docker compose up -d # start all services
docker compose down # stop + remove containers
docker compose down -v # also remove volumes
docker compose logs -f # stream all logs
docker compose logs -f web # stream one service
docker compose ps # list services
docker compose exec web bash # shell into service
docker compose build --no-cache # force rebuild
docker compose pull # pull latest images
📌 Command Cheat Sheet
| Task | Command |
|---|---|
| Start container in background | docker run -d -p 8080:80 nginx |
| Shell into running container | docker exec -it NAME bash |
| Stream logs | docker logs -f NAME |
| Stop & remove container | docker rm -f NAME |
| Remove all stopped containers | docker container prune |
| Build image | docker build -t myapp:1.0 . |
| Push to registry | docker push user/myapp:1.0 |
| Show disk usage | docker system df |
| Clean everything unused | docker system prune -a --volumes |