Back to blog
Cloud & DevOpsintermediate

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.

LearnixoApril 13, 202610 min read
View Source
DockerKubernetesDevOpsAzureContainersHelm
Share:𝕏

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

Enjoyed this article?

Explore the Cloud & DevOps learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.