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
# Mac/Windows: install Docker Desktop
# Linux:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USERKey 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
# 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
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
# 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 apiDockerfile Best Practices
# 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.
# 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# 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 -vKubernetes 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
# 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-secretService
# 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
# 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: 80ConfigMap and Secrets
# 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
# 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: 80Applying Manifests
# 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)
# 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.
# 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# 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# 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 productionGitHub Actions CI/CD Pipeline
# .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 5mObservability in Kubernetes
# 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 monitoringWhat to Learn Next
- System Design: architect the full distributed system
- Azure DevOps: enterprise CI/CD with Azure Pipelines
- Observability: logging, metrics, tracing in production