Every developer has heard some version of this story: "It works on my machine." The code runs perfectly locally. It's been tested. QA signed off. Then it hits production and... nothing works. Different OS, different Python version, missing libraries, conflicting dependencies.

Docker's solution to this problem is elegantly simple: ship the machine too.

What Is Docker, Really?

Docker packages your application and everything it needs — runtime, libraries, config, environment variables — into a single standardized unit called a container. Containers are:

  • Isolated — They don't interfere with each other or with the host system
  • Portable — A container that runs on your laptop runs identically in production
  • Lightweight — Unlike VMs, containers share the host OS kernel. Startup is measured in milliseconds, not minutes
  • Reproducible — The Dockerfile is a recipe that builds the same image every time

A container is an instance of an image. An image is built from a Dockerfile. Think of it: Dockerfile → Image → Container :: Source code → Executable → Running process.

Your First Dockerfile

Let's containerize a simple Python web app:

# Start from an official base image
FROM python:3.12-slim

# Set working directory inside the container
WORKDIR /app

# Copy dependency file first (for layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application
COPY . .

# Tell Docker which port the app listens on
EXPOSE 8000

# The command to run when the container starts
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Layer Caching Tip
Docker caches each instruction as a "layer." Copy requirements.txt and install dependencies before copying your app code. That way, if only your code changes, Docker skips the slow dependency installation step.

Build and run:

# Build an image tagged "my-app"
docker build -t my-app .

# Run a container from that image
docker run -p 8000:8000 my-app

# Run in background (detached)
docker run -d -p 8000:8000 --name my-app-container my-app

# See running containers
docker ps

# Stop it
docker stop my-app-container

Docker Compose: Multiple Containers

Real apps rarely run in a single container. You've got a web server, a database, a cache, a message queue. Docker Compose lets you define and run multi-container apps:

version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache
    volumes:
      - ./src:/app/src  # mount local code for development

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data

  cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru

volumes:
  postgres_data:
docker compose up -d   # Start everything in background
docker compose logs -f  # Follow logs
docker compose down    # Stop and remove containers

Enter Kubernetes: Containers at Scale

Docker Compose is great for development and small deployments. But what happens when you need to:

  • Run 50 instances of your web service across multiple servers?
  • Automatically restart containers that crash?
  • Roll out updates with zero downtime?
  • Scale up when traffic spikes and scale down to save money?

That's Kubernetes (K8s). It's a container orchestrator — a system for deploying, managing, and scaling containerized workloads across clusters of machines.

Core K8s Concepts

Kubernetes has a steep learning curve, but it builds on a few core abstractions:

  • Pod — The smallest deployable unit. Usually one container, sometimes a few tightly coupled ones.
  • Deployment — Declares the desired state: "I want 3 replicas of this pod running at all times."
  • Service — A stable network endpoint for a set of pods. Pods come and go; Services persist.
  • Ingress — Routes external traffic into Services based on hostname/path rules.
  • ConfigMap / Secret — Environment-specific configuration, decoupled from the container image.
  • Namespace — Virtual cluster for isolating resources (dev/staging/prod in one physical cluster).

Your First K8s Deployment

First, get a local Kubernetes cluster. kind (Kubernetes in Docker) is the easiest:

# Install kind (on macOS/Linux)
brew install kind
# or
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64 && chmod +x ./kind

# Create a cluster
kind create cluster --name my-cluster

# Verify
kubectl get nodes

Now write a deployment YAML:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: my-app:latest
        ports:
        - containerPort: 8000
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "500m"
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  selector:
    app: my-app
  ports:
  - port: 80
    targetPort: 8000
  type: ClusterIP
kubectl apply -f deployment.yaml
kubectl get pods                      # See running pods
kubectl get deployments               # Deployment status
kubectl rollout status deployment/my-app  # Wait for rollout
kubectl logs -f deployment/my-app     # Stream logs

Zero-Downtime Updates

One of Kubernetes' killer features is rolling updates:

# Update the image
kubectl set image deployment/my-app my-app=my-app:v2

# K8s will gradually replace pods with the new version
# keeping at least N replicas healthy throughout

# Something broke? Roll back instantly
kubectl rollout undo deployment/my-app

Where to Host Your Cluster

For production, managed Kubernetes services eliminate the operational burden of running control planes:

  • Google Kubernetes Engine (GKE) — The most mature, K8s originated at Google
  • Amazon EKS — Best if you're already in the AWS ecosystem
  • Azure AKS — Great Azure/enterprise integration
  • DigitalOcean DOKS — Simpler, cheaper, great for smaller projects
  • Cloudflare + Workers — For edge-native deployments without managing nodes at all
🚦
Don't Over-Kubernetes
Kubernetes adds real operational complexity. If you're running a side project or a small app with a handful of users, Docker Compose on a single VPS is probably the right answer. Use K8s when you genuinely need orchestration, not because it sounds cool.

Next Steps

You now understand containers (Docker) and orchestration (Kubernetes) at a conceptual and practical level. The path forward:

  1. Practice with kind locally — deploy a real multi-tier app
  2. Learn Helm (the Kubernetes package manager)
  3. Explore namespaces, RBAC, and NetworkPolicy for production hardening
  4. Set up a GitOps workflow with ArgoCD or Flux
  5. Get certified: CKA (Certified Kubernetes Administrator) is the gold standard

The container ecosystem is vast but extremely well-documented. You're now equipped to navigate it.

TF Editorial

TF Editorial

Editorial Team · Tomfoolering

We write about technology with depth and without condescension.