Docker & Kubernetes: From Zero to Production Deployment
Master Docker and Kubernetes from scratch. Learn containerisation, Docker Compose, Kubernetes deployments, services, ingress, Helm, and deploying .NET and Python apps to Azure Kubernetes Service.
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
Enjoyed this article?
Explore the Cloud & DevOps learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.