Docker Fundamentals#
Introduction#
Docker packages applications with all their dependencies into containers — lightweight, portable units that run consistently across any environment. Whether you’re developing on macOS, testing in CI, or deploying to production on Linux, the container behaves identically.
Why Docker Matters:
Consistency: “Works on my machine” becomes “works everywhere”
Isolation: Each container has its own filesystem, network, and process space
Reproducibility: Pin exact versions of OS, language runtime, and dependencies
Fast onboarding: New developers run
docker compose upinstead of installing dozens of tools
Core Concepts#
graph TD
A[Dockerfile] -->|docker build| B[Image]
B -->|docker run| C[Container]
B -->|docker push| D[Registry<br>Docker Hub / GitLab Registry]
D -->|docker pull| B
Concept |
Description |
Analogy |
|---|---|---|
Dockerfile |
Recipe for building an image |
A cooking recipe |
Image |
Read-only template with app + dependencies |
A frozen meal |
Container |
Running instance of an image |
The meal, heated and served |
Registry |
Storage for images |
A grocery store |
Volume |
Persistent storage outside the container |
A USB drive |
Network |
Communication between containers |
A LAN cable |
Dockerfile Best Practices#
Basic Dockerfile (Python/FastAPI)#
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first (leverages Docker layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY src/ ./src/
# Run as non-root user (security best practice)
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Multi-Stage Build (Smaller Images)#
Multi-stage builds produce smaller production images by separating the build environment from the runtime:
# Stage 1: Build
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt
# Stage 2: Runtime (smaller base image)
FROM python:3.12-slim
WORKDIR /app
# Copy only the installed packages from the builder stage
COPY --from=builder /app/deps /usr/local/lib/python3.12/site-packages/
COPY src/ ./src/
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Layer Caching Rules#
Docker caches each layer. If a layer changes, all subsequent layers are rebuilt. Order your instructions from least frequently changed to most frequently changed:
# GOOD order (cache-friendly):
FROM python:3.12-slim # Rarely changes
WORKDIR /app # Never changes
COPY requirements.txt . # Changes when deps change
RUN pip install -r req... # Cached unless requirements.txt changed
COPY src/ ./src/ # Changes on every code change (last!)
# BAD order (breaks cache on every change):
FROM python:3.12-slim
COPY . . # Everything changes → pip install runs every time
RUN pip install -r requirements.txt
Dockerfile Checklist#
Use specific image tags (
python:3.12-slim), neverlatestCopy dependency files before source code (layer caching)
Use
--no-cache-dirwith pip (smaller image)Run as non-root user (
USER appuser)Use
.dockerignoreto exclude unnecessary filesUse multi-stage builds for production images
Set
EXPOSEfor documentation (does not publish ports)
.dockerignore#
.git
.venv
__pycache__
*.pyc
.env
.env.*
node_modules
.pytest_cache
.coverage
*.egg-info
dist/
build/
Docker Compose for Development#
Docker Compose defines multi-container applications in a single docker-compose.yml file.
FastAPI + PostgreSQL + Redis#
# docker-compose.yml
services:
app:
build: .
ports:
- "8000:8000"
volumes:
- ./src:/app/src # Hot reload: code changes reflect immediately
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata: # Named volume persists data across container restarts
Essential Commands#
# Start all services (foreground)
docker compose up
# Start in background
docker compose up -d
# Rebuild images after Dockerfile changes
docker compose up --build
# Stop all services
docker compose down
# Stop and remove volumes (fresh database)
docker compose down -v
# View logs
docker compose logs -f app
# Execute command in running container
docker compose exec app bash
# Run a one-off command
docker compose run --rm app pytest tests/
Volume Types#
Type |
Use Case |
Example |
|---|---|---|
Named volume |
Persistent data (databases) |
|
Bind mount |
Development hot-reload |
|
tmpfs |
Temporary data (secrets, caches) |
|
Named volumes persist across container restarts and are managed by Docker. Bind mounts map host directories into the container and are essential for development workflows where you want live code reloading.
Networking#
Containers in the same docker-compose.yml can communicate using service names as hostnames:
# Inside the 'app' container, connect to PostgreSQL:
DATABASE_URL = "postgresql://postgres:postgres@db:5432/myapp"
# ^^
# service name = hostname
Common Networking Patterns#
Pattern |
When to Use |
|---|---|
|
Expose to host machine (development, external access) |
No |
Service only accessible to other containers (internal database) |
Custom network |
Isolate groups of services from each other |
Security#
Image Security#
# Scan image for vulnerabilities
docker scout cves myapp:latest
# Alternative: Trivy (open source)
trivy image myapp:latest
Runtime Security Checklist#
Run as non-root user (
USER appuserin Dockerfile)Use read-only filesystem where possible (
read_only: truein compose)Never store secrets in images (use environment variables or Docker secrets)
Pin image versions (
postgres:16-alpine, notpostgres:latest)Scan images in CI before deployment
Use minimal base images (
-slim,-alpine)Drop unnecessary Linux capabilities
Debugging Containers#
# Check running containers
docker ps
# View container logs
docker logs <container_id> --tail 50
# Shell into a running container
docker exec -it <container_id> bash
# Inspect container configuration
docker inspect <container_id>
# Check resource usage
docker stats
# View container filesystem changes
docker diff <container_id>
Summary#
Topic |
Key Takeaway |
|---|---|
Images |
Use specific tags, multi-stage builds, non-root user |
Compose |
Define multi-container dev environment in one file |
Volumes |
Named volumes for data persistence, bind mounts for development |
Networking |
Service names are hostnames within Compose |
Security |
Scan images, run non-root, never embed secrets |
Caching |
Order Dockerfile layers from least to most frequently changed |
References#
Practice#
Exercise 1: Containerize a FastAPI App#
Given a FastAPI project with this structure:
myapp/
├── src/
│ ├── main.py
│ └── config.py
├── tests/
│ └── test_main.py
├── requirements.txt
└── README.md
Tasks:
Write a
Dockerfilefollowing all best practices from this guideWrite a
docker-compose.ymlwith the app + PostgreSQLVerify the app starts and connects to the database
Add a health check for the database service
Exercise 2: Optimize Image Size#
Start with this unoptimized Dockerfile:
FROM python:3.12
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0"]
Tasks:
Measure the image size (
docker images)Apply optimizations: slim base, multi-stage build, .dockerignore, non-root user
Measure the new image size
Target: reduce image size by at least 50%
Exercise 3: Debug a Container#
A container starts but the API returns 500 errors. Walk through these debugging steps:
Check container logs (
docker logs)Shell into the container and verify environment variables
Test database connectivity from inside the container
Check if the correct port is exposed
Review Questions#
What is the difference between a Docker image and a container?
Hint: Think of the relationship between a class and an object in OOP.
Why should you copy
requirements.txtbefore copying source code in a Dockerfile?Hint: Think about Docker layer caching and what triggers a rebuild.
What is a multi-stage build and when would you use one?
Hint: Consider what tools you need at build time vs runtime.
What is the difference between a named volume and a bind mount? When would you use each?
Hint: One is for persistent data, the other is for development convenience.
Why should containers run as a non-root user?
Hint: Think about what happens if an attacker exploits a vulnerability in your app.
How do containers in the same Docker Compose file communicate with each other?
Hint: Docker creates a network and uses service names as DNS hostnames.