Docker & Kubernetes · Lesson 1 of 1

Docker & Kubernetes: Zero to Production

Why Containers?

Before containers: "it works on my machine" was a real problem. Different OS, different runtime versions, different environment variables — endless inconsistencies between dev, test, and production.

Containers solve this: package your application and all its dependencies into a portable, reproducible unit. The same container runs identically on your laptop, in CI, and in production.

Docker creates and runs containers. Kubernetes (K8s) orchestrates many containers across many machines — handles scaling, health checks, rolling updates, and service discovery.


Docker Fundamentals

Installing Docker

Bash
# Mac/Windows: install Docker Desktop
# Linux:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Key concepts

| Concept | What it is | |---------|-----------| | Image | Read-only blueprint. Like a class in OOP. | | Container | Running instance of an image. Like an object. | | Dockerfile | Instructions to build an image. | | Registry | Storage for images (Docker Hub, Azure Container Registry). | | Layer | Each Dockerfile instruction creates a cached layer. |


Writing a Dockerfile

.NET 8 API

DOCKERFILE
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy project files first (cached if unchanged)
COPY ["OrderFlow.Api/OrderFlow.Api.csproj", "OrderFlow.Api/"]
RUN dotnet restore "OrderFlow.Api/OrderFlow.Api.csproj"

# Copy source and build
COPY . .
WORKDIR "/src/OrderFlow.Api"
RUN dotnet build -c Release -o /app/build
RUN dotnet publish -c Release -o /app/publish --no-restore

# Stage 2: Runtime (much smaller  no SDK)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app

# Non-root user for security
RUN adduser --disabled-password --no-create-home appuser
USER appuser

COPY --from=build /app/publish .

EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080

ENTRYPOINT ["dotnet", "OrderFlow.Api.dll"]

Python FastAPI

DOCKERFILE
FROM python:3.12-slim

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Install Python dependencies (cached layer if requirements.txt unchanged)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Non-root user
RUN useradd --no-create-home appuser && chown -R appuser /app
USER appuser

COPY --chown=appuser:appuser . .

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8000/health || exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Building and running

Bash
# Build image
docker build -t orderflow-api:latest .

# Run container
docker run -p 8080:8080 \
  -e ConnectionStrings__Default="Server=host.docker.internal;..." \
  -e Jwt__Secret="my-secret" \
  orderflow-api:latest

# Run in background
docker run -d --name api -p 8080:8080 orderflow-api:latest

# View running containers
docker ps

# View logs
docker logs api -f

# Stop and remove
docker stop api && docker rm api

Dockerfile Best Practices

DOCKERFILE
# 1. Use specific tags, never "latest" in production
FROM mcr.microsoft.com/dotnet/aspnet:8.0.4-alpine

# 2. .dockerignore  exclude unnecessary files
# .dockerignore:
# **/.git
# **/node_modules
# **/.env
# **/bin
# **/obj
# **/*.user

# 3. Order layers by change frequency (least frequent first)
COPY package.json .          # rarely changes
RUN npm ci                   # cached when package.json unchanged
COPY . .                     # changes often  invalidates cache from here

# 4. Combine RUN commands to reduce layers
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*  # clean up in same layer!

# 5. Use COPY --chown instead of RUN chown (avoids extra layer)
COPY --chown=appuser:appuser . .

Docker Compose

Compose manages multi-container applications locally.

YAML
# docker-compose.yml
version: "3.9"

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__Default=Server=db;Database=OrderFlow;User Id=sa;Password=${DB_PASSWORD};TrustServerCertificate=true
      - Jwt__Secret=${JWT_SECRET}
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - orderflow

  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=${DB_PASSWORD}
    ports:
      - "1433:1433"
    volumes:
      - sqldata:/var/opt/mssql
    healthcheck:
      test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$$SA_PASSWORD" -Q "SELECT 1" || exit 1
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - orderflow

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    networks:
      - orderflow

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/ssl/certs:ro
    depends_on:
      - api
    networks:
      - orderflow

volumes:
  sqldata:
  redisdata:

networks:
  orderflow:
    driver: bridge
Bash
# Start everything
docker compose up -d

# View logs
docker compose logs -f api

# Scale the API (3 instances)
docker compose up -d --scale api=3

# Stop and remove
docker compose down

# Stop and remove with volumes (wipe data)
docker compose down -v

Kubernetes Architecture

                          ┌─────────────────────────────────────────┐
                          │              Kubernetes Cluster           │
                          │                                           │
  Client Request ────────►│  Ingress Controller                      │
                          │       │                                   │
                          │  ┌────▼────────────────────────────────┐ │
                          │  │            Service                   │ │
                          │  │        (load balances)               │ │
                          │  └────┬────────┬────────┬──────────────┘ │
                          │       │        │        │                 │
                          │  ┌────▼──┐ ┌──▼───┐ ┌──▼───┐            │
                          │  │  Pod  │ │  Pod │ │  Pod │            │
                          │  │  API  │ │  API │ │  API │            │
                          │  └───────┘ └──────┘ └──────┘            │
                          └─────────────────────────────────────────┘

Core objects:

  • Pod: smallest deployable unit — one or more containers
  • Deployment: declares desired pod count, handles rolling updates
  • Service: stable DNS name and IP that routes to pods
  • Ingress: HTTP routing rules, SSL termination
  • ConfigMap: non-secret configuration
  • Secret: sensitive configuration (base64 encoded)
  • PersistentVolumeClaim: request for storage

Kubernetes Manifests

Deployment

YAML
# k8s/api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orderflow-api
  namespace: production
  labels:
    app: orderflow-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: orderflow-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1           # allow 1 extra pod during update
      maxUnavailable: 0     # never reduce below replicas count
  template:
    metadata:
      labels:
        app: orderflow-api
    spec:
      containers:
        - name: api
          image: myregistry.azurecr.io/orderflow-api:1.2.3
          ports:
            - containerPort: 8080
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Production
            - name: ConnectionStrings__Default
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: db-connection-string
            - name: Jwt__Secret
              valueFrom:
                secretKeyRef:
                  name: orderflow-secrets
                  key: jwt-secret
          resources:
            requests:
              cpu: 100m       # 0.1 CPU
              memory: 128Mi
            limits:
              cpu: 500m       # 0.5 CPU
              memory: 512Mi
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 30
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            readOnlyRootFilesystem: true
      imagePullSecrets:
        - name: acr-secret

Service

YAML
# k8s/api-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: orderflow-api-svc
  namespace: production
spec:
  selector:
    app: orderflow-api        # routes to pods with this label
  ports:
    - protocol: TCP
      port: 80                # service port
      targetPort: 8080        # container port
  type: ClusterIP             # internal only (Ingress handles external)

Ingress

YAML
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: orderflow-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.orderflow.io
      secretName: orderflow-tls
  rules:
    - host: api.orderflow.io
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: orderflow-api-svc
                port:
                  number: 80

ConfigMap and Secrets

YAML
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: orderflow-config
  namespace: production
data:
  ASPNETCORE_ENVIRONMENT: "Production"
  LOG_LEVEL: "Information"

---
# k8s/secrets.yaml (in practice, use Azure Key Vault or Sealed Secrets)
apiVersion: v1
kind: Secret
metadata:
  name: orderflow-secrets
  namespace: production
type: Opaque
data:
  # base64: echo -n "value" | base64
  db-connection-string: U2VydmVyPW15c3FsOw==
  jwt-secret: c3VwZXItc2VjcmV0LWtleQ==

Horizontal Pod Autoscaler

YAML
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: orderflow-api-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: orderflow-api
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

Applying Manifests

Bash
# Apply all files in directory
kubectl apply -f k8s/

# Check deployment status
kubectl rollout status deployment/orderflow-api -n production

# View pods
kubectl get pods -n production -w

# View pod logs
kubectl logs -f deployment/orderflow-api -n production

# Exec into a pod
kubectl exec -it pod/orderflow-api-xxx -- /bin/sh

# Scale manually
kubectl scale deployment orderflow-api --replicas=5 -n production

# Rolling update (change image)
kubectl set image deployment/orderflow-api \
  api=myregistry.azurecr.io/orderflow-api:1.2.4 -n production

# Rollback
kubectl rollout undo deployment/orderflow-api -n production

# Delete everything
kubectl delete -f k8s/

Deploy to Azure Kubernetes Service (AKS)

Bash
# Install Azure CLI + kubectl
az login
az extension add --name aks-preview

# Create resource group
az group create --name orderflow-rg --location uksouth

# Create AKS cluster
az aks create \
  --resource-group orderflow-rg \
  --name orderflow-aks \
  --node-count 3 \
  --node-vm-size Standard_B2s \
  --enable-managed-identity \
  --enable-oidc-issuer \
  --generate-ssh-keys

# Get credentials
az aks get-credentials --resource-group orderflow-rg --name orderflow-aks

# Create Azure Container Registry
az acr create --resource-group orderflow-rg --name orderflowacr --sku Basic

# Attach ACR to AKS (so AKS can pull images)
az aks update --name orderflow-aks --resource-group orderflow-rg \
  --attach-acr orderflowacr

# Build and push image to ACR
az acr build --registry orderflowacr --image orderflow-api:1.0.0 .

# Deploy
kubectl apply -f k8s/

Helm Charts

Helm is a package manager for Kubernetes. Instead of managing many YAML files, define a chart with templates.

Bash
# Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Create a chart
helm create orderflow

# Chart structure:
# orderflow/
# ├── Chart.yaml
# ├── values.yaml          # default values
# └── templates/
#     ├── deployment.yaml
#     ├── service.yaml
#     └── ingress.yaml
YAML
# values.yaml
replicaCount: 3

image:
  repository: orderflowacr.azurecr.io/orderflow-api
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  host: api.orderflow.io
  tls: true

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 20
  targetCPUUtilizationPercentage: 70
Bash
# Install chart
helm install orderflow ./orderflow -n production --create-namespace

# Upgrade
helm upgrade orderflow ./orderflow -n production \
  --set image.tag=1.2.4

# Rollback
helm rollback orderflow 1 -n production

# View history
helm history orderflow -n production

GitHub Actions CI/CD Pipeline

YAML
# .github/workflows/deploy.yml
name: Build and Deploy to AKS

on:
  push:
    branches: [main]

env:
  REGISTRY: orderflowacr.azurecr.io
  IMAGE_NAME: orderflow-api

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.tag.outputs.tag }}
    steps:
      - uses: actions/checkout@v4

      - name: Set image tag
        id: tag
        run: echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT

      - name: Log in to ACR
        uses: azure/docker-login@v1
        with:
          login-server: ${{ env.REGISTRY }}
          username: ${{ secrets.ACR_USERNAME }}
          password: ${{ secrets.ACR_PASSWORD }}

      - name: Build and push
        run: |
          docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} .
          docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Azure login
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Get AKS credentials
        run: az aks get-credentials --resource-group orderflow-rg --name orderflow-aks

      - name: Deploy with Helm
        run: |
          helm upgrade orderflow ./helm/orderflow \
            --namespace production \
            --set image.tag=${{ needs.build-and-push.outputs.image-tag }} \
            --wait \
            --timeout 5m

Observability in Kubernetes

Bash
# View resource usage
kubectl top pods -n production
kubectl top nodes

# View events (useful for debugging)
kubectl get events -n production --sort-by='.lastTimestamp'

# Describe a failing pod
kubectl describe pod orderflow-api-xxx -n production

# Port forward to debug locally
kubectl port-forward deployment/orderflow-api 8080:8080 -n production

# Install Prometheus + Grafana (via Helm)
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus prometheus-community/kube-prometheus-stack -n monitoring

What to Learn Next

  • System Design: architect the full distributed system
  • Azure DevOps: enterprise CI/CD with Azure Pipelines
  • Observability: logging, metrics, tracing in production