Containers, images, volumes, networks — and why your ops team will love you for it.
Same isolation goal — very different mechanism.
Containers are just regular Linux processes — wrapped in isolation primitives.
Limit CPU, RAM, and I/O per container. The kernel enforces quotas — no runaway process can starve another.
Full isolation without a second OS. Starts in under a second. Shares the host kernel efficiently and safely.
An image is a blueprint. A container is a running instance of that blueprint.
Images bundle their own userland — only the kernel is borrowed from the host.
Linux has a stable syscall ABI. Every app ultimately calls syscall. The kernel responds. The C library version comes from the image, not the host — Debian glibc runs fine on an Alpine host kernel.
You cannot run an ARM64 image on an x86-64 host without QEMU emulation. Kernel ABI is stable but architecture-specific.
A recipe: each instruction bakes a new read-only layer into the image.
# Start from an official base image FROM python:3.12-slim # Set working directory inside the container WORKDIR /app # Copy dependency list FIRST (layer cache trick!) COPY requirements.txt . # Install — cached if req.txt unchanged RUN pip install --no-cache-dir -r requirements.txt # Copy the rest of the app code COPY . . # Document which port the app listens on EXPOSE 8000 # Set an environment variable ENV DEBUG=false # Default command when container starts CMD ["python", "-m", "uvicorn", "main:app"]
# Build and tag docker build -t myapp:1.0 . # Run, mapping host :8080 → container :8000 docker run -p 8080:8000 myapp:1.0
Put rarely-changing steps first. COPY requirements.txt before COPY . . so slow installs only re-run when the list changes.
Container filesystems are ephemeral. Volumes live outside the container lifecycle.
Containers have their own network stack. Traefik handles HTTPS — the apps inside speak plain HTTP.
Define your whole stack in one YAML file. One command to start everything.
# docker-compose.yml services: traefik: image: traefik:v3 command: - "--providers.docker=true" - "--entrypoints.websecure.address=:443" - "--entrypoints.websecure.http.tls=true" ports: - "443:443" # only public port (HTTPS) volumes: - "/var/run/docker.sock:/var/run/docker.sock" - "./certs:/certs:ro" # cert.pem + key.pem networks: [mynet] app: build: . labels: - "traefik.http.routers.app.rule=Host(`app.example.com`)" - "traefik.http.routers.app.entrypoints=websecure" networks: [mynet] depends_on: [db] environment: - DATABASE_URL=postgres://user:pass@db:5432/mydb db: image: postgres:16-alpine environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: mydb volumes: - pgdata:/var/lib/postgresql/data networks: [mynet] # no ports: → stays private volumes: pgdata: networks: mynet:
# Start everything (detached) docker compose up -d # Stream logs from all services docker compose logs -f # Stop & remove containers + network docker compose down
./certs/No ports: on db. App reaches it as db:5432 on the internal network. Internet never touches it.
Traefik is the single HTTPS gateway — apps inside talk plain HTTP to each other.
The philosophy shift that makes containers so transformative for operations.
Traditional VMs / bare-metal ops
docker compose restart app--scale app=10Docker / Kubernetes / modern ops
Immutable infrastructure wins
Everything needed to containerise a real app today.
One YAML, one command: docker compose up -d. Traefik handles HTTPS on :443, holds the cert & key, decrypts traffic and forwards plain HTTP internally. Postgres stays private.
Treat containers as disposable identical units. Kill & replace instead of nurse & patch. Immutable infrastructure = fewer 3am pages.