Skip to main content

🐳 Part 11: Deploying .NET 8 Microservices with Docker & Kubernetes

You’ve built solid microservices. Now they need a home.

In this part, we’ll:

✅ Containerize .NET 8 services with Docker
✅ Run locally with Docker Compose
✅ Deploy to Kubernetes with readiness/liveness probes, scaling, and Ingress
✅ Manage configuration securely with ConfigMaps & Secrets
✅ Add autoscaling and outline CI/CD

By the end, your microservices will be cloud-ready & production-ready 🚀


1️⃣ Containerize a .NET 8 Service (Multi-Stage Build)

Let’s containerize product-service with a multi-stage Dockerfile.

📄 ProductService/Dockerfile

# ---------- Build stage ----------
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# copy sln and csproj for cached restores
COPY ProductService/*.csproj ProductService/
RUN dotnet restore ProductService/ProductService.csproj

# copy the rest
COPY ProductService/. ProductService/
WORKDIR /src/ProductService
RUN dotnet publish -c Release -o /app /p:UseAppHost=false

# ---------- Runtime stage ----------
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app

# security: run as non-root
RUN addgroup --system app && adduser --system --ingroup app app
USER app

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

COPY --from=build /app ./
ENTRYPOINT ["dotnet", "ProductService.dll"]

👉 Build & run locally:

docker build -t product-service:1.0 -f ProductService/Dockerfile .
docker run -p 8081:8080 --name product product-service:1.0
# Test
curl http://localhost:8081/health

✅ Best practices included:

  • Multi-stage → small, fast images
  • Run as non-root
  • ASPNETCORE_URLS bound to container port
  • Uses .NET 8 base images

2️⃣ Local Dev with Docker Compose

📄 docker-compose.yml

version: "3.9"
services:
  product-service:
    build:
      context: .
      dockerfile: ProductService/Dockerfile
    image: product-service:1.0
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - Redis__ConnectionString=redis:6379
      - Rabbit__Host=rabbitmq
    ports:
      - "8081:8080"
    depends_on:
      - redis
      - rabbitmq

  order-service:
    build:
      context: .
    image: order-service:1.0
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - Rabbit__Host=rabbitmq
    ports:
      - "8082:8080"
    depends_on:
      - rabbitmq

  redis:
    image: redis:7
    ports: ["6379:6379"]

  rabbitmq:
    image: rabbitmq:3.13-management
    ports:
      - "5672:5672"
      - "15672:15672"  # UI: http://localhost:15672 (guest/guest)

Run everything:

docker compose up --build

Test:

curl http://localhost:8081/ready
curl http://localhost:8082/health

3️⃣ Push Images to a Registry

docker tag product-service:1.0 yourdockerhub/product-service:1.0
docker push yourdockerhub/product-service:1.0

(Repeat for order-service.)


4️⃣ Kubernetes Namespace

kubectl create namespace shop

5️⃣ ConfigMaps & Secrets

📄 k8s/config.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: product-config
  namespace: shop
data:
  ASPNETCORE_ENVIRONMENT: "Production"
  Redis__ConnectionString: "redis:6379"
  Rabbit__Host: "rabbitmq"

📄 k8s/secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: product-secrets
  namespace: shop
type: Opaque
stringData:
  ConnectionStrings__AppDb: "Server=sql;Database=ProductsDb;User Id=sa;Password=Your_P@ss123;TrustServerCertificate=true"

Apply:

kubectl apply -f k8s/config.yaml
kubectl apply -f k8s/secret.yaml

👉 For production → use Azure Key Vault / AWS Secrets Manager.


6️⃣ Deployment + Service + Probes

📄 k8s/product-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
  namespace: shop
spec:
  replicas: 2
  selector:
    matchLabels: { app: product-service }
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels: { app: product-service }
    spec:
      containers:
        - name: product-service
          image: yourdockerhub/product-service:1.0
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef: { name: product-config }
            - secretRef: { name: product-secrets }
          readinessProbe:
            httpGet: { path: /ready, port: 8080 }
            initialDelaySeconds: 5
          livenessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 10
          resources:
            requests: { cpu: "100m", memory: "128Mi" }
            limits: { cpu: "500m", memory: "512Mi" }
          securityContext:
            runAsUser: 1000
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
---
apiVersion: v1
kind: Service
metadata:
  name: product-service
  namespace: shop
spec:
  selector: { app: product-service }
  ports:
    - port: 80
      targetPort: 8080

Apply:

kubectl apply -f k8s/product-deploy.yaml

7️⃣ Autoscaling with HPA

📄 k8s/product-hpa.yaml

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: product-service
  namespace: shop
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: product-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

Apply:

kubectl apply -f k8s/product-hpa.yaml

👉 For queue/event-driven workloads → use KEDA.


8️⃣ Ingress (NGINX) + TLS

📄 k8s/ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ingress
  namespace: shop
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts: [ "shop.local" ]
      secretName: shop-tls
  rules:
    - host: shop.local
      http:
        paths:
          - path: /products
            pathType: Prefix
            backend:
              service:
                name: product-service
                port: { number: 80 }
          - path: /orders
            pathType: Prefix
            backend:
              service:
                name: order-service
                port: { number: 80 }

Generate self-signed TLS (dev only):

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout shop.key -out shop.crt \
  -subj "/CN=shop.local/O=dev"

kubectl create secret tls shop-tls -n shop --key shop.key --cert shop.crt

9️⃣ Rolling Updates & Zero-Downtime

  • Readiness probe → traffic only to healthy pods
  • Liveness probe → restarts hung pods

Rollback if needed:

kubectl rollout status deploy/product-service -n shop
kubectl rollout undo deploy/product-service -n shop

🔟 CI/CD Outline (GitHub Actions)

📄 .github/workflows/cd.yml

name: build-and-deploy
on:
  push:
    branches: [ "main" ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: '8.0.x' }
      - run: dotnet test --nologo
      - run: docker build -t ghcr.io/you/product-service:${{ github.sha }} -f ProductService/Dockerfile .
      - run: echo $CR_PAT | docker login ghcr.io -u you --password-stdin
      - run: docker push ghcr.io/you/product-service:${{ github.sha }}
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: azure/setup-kubectl@v4
      - run: |
          kubectl set image deploy/product-service -n shop product-service=ghcr.io/you/product-service:${{ github.sha }}
          kubectl rollout status deploy/product-service -n shop

👉 For structured deployments → use Helm or Kustomize.


✅ Production Checklist

  • [ ] Run containers as non-root
  • [ ] Readiness & liveness probes tuned
  • [ ] Resource requests/limits set → enable HPA
  • [ ] Centralized logs + traces + metrics
  • [ ] Secrets from vaults (not configs)
  • [ ] Network Policies (deny-all → allow needed)
  • [ ] PodDisruptionBudgets for HA
  • [ ] Blue/green or canary deploys (Ingress / Argo Rollouts)
  • [ ] Backups + DR plan

🎯 Recap

You now have a repeatable deployment path for .NET 8 microservices:

  • Build secure, small images (multi-stage, non-root)
  • Run locally with docker-compose
  • Deploy to Kubernetes with probes, scaling, Ingress & TLS
  • Automate with CI/CD + Helm/Kustomize

✨ In the final part (Part 12) → we’ll lock it all down with: Link


Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...