M-Bus Gateway
← Tilbage til blog
· Kubernetes· Docker· DevOps· migration· SaaS· Helm· deployment· skalering

Fra Docker Compose til Kubernetes — hvornår og hvordan

Migrering fra Docker Compose til Kubernetes: hvornår det giver mening, Deployment/Service/Ingress basics, Helm charts, Persistent Volumes og zero-downtime deployment.

Af M-Bus Gateway

Kubernetes er ikke altid bedre end Docker Compose. Her er hvornår du bør migrere, og de grundlæggende mønstre.


Hvornår giver Kubernetes mening?

Docker Compose er bedre ved:
  → Under 10 containers
  → Enkelt server (VPS)
  → Team under 5 udviklere
  → Ingen komplekse skaleringsscenarier
  → Budget < 50.000 DKK/måned infrastruktur

Kubernetes giver merværdi ved:
  → Multi-node deployment (HA)
  → Auto-scaling (HPA/VPA)
  → Rolling updates uden downtime (nemt i K8s, sværere i Compose)
  → Service mesh (mTLS, observability)
  → Multi-region deployment
  → Kubernetes-native tooling (Helm, Argo CD, Flux)

M-Bus Gateway nuværende status:
  → Docker Compose på Hetzner VPS → KORREKT VALG nu
  → Fremtid: Kubernetes ved > 100 tenants og multi-region krav
  → Migration estimat: 3-6 måneder fuld migration

TL;DR: Start med Docker Compose. Migrer til Kubernetes når
  A) Du er nødt til det (scale), eller
  B) Din infrastruktur er for kompleks til Compose

Kubernetes objekter: Det minimale sæt

# server-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mbus-server
  namespace: mbus-production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mbus-server
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # Max 1 ekstra pod under update
      maxUnavailable: 0  # Nul downtime: Ingen pods fjernes før nye er klar
  template:
    metadata:
      labels:
        app: mbus-server
    spec:
      containers:
        - name: server
          image: ghcr.io/kirken20/mbus-gateway/server:latest
          ports:
            - containerPort: 8000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: mbus-secrets
                  key: database-url
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: mbus-secrets
                  key: jwt-secret
          resources:
            requests:
              cpu: "250m"     # 0.25 vCPU
              memory: "256Mi"
            limits:
              cpu: "1000m"    # 1 vCPU max
              memory: "1Gi"
          livenessProbe:
            httpGet:
              path: /api/health
              port: 8000
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /api/health
              port: 8000
            initialDelaySeconds: 10
            periodSeconds: 5
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000

Service og Ingress

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mbus-server-svc
  namespace: mbus-production
spec:
  selector:
    app: mbus-server
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8000
  type: ClusterIP  # Intern — eksponeres via Ingress

---
# ingress.yaml (nginx-ingress-controller)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mbus-ingress
  namespace: mbus-production
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.mbus-gateway.dk
      secretName: mbus-tls-cert
  rules:
    - host: api.mbus-gateway.dk
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: mbus-server-svc
                port:
                  number: 80

Secrets og ConfigMaps

# secrets.yaml (ALDRIG commit til git — brug Sealed Secrets eller External Secrets)
apiVersion: v1
kind: Secret
metadata:
  name: mbus-secrets
  namespace: mbus-production
type: Opaque
stringData:
  database-url: "postgresql+asyncpg://mbus:password@postgres:5432/mbus"
  jwt-secret: "your-jwt-secret-min-64-chars"
  stripe-secret-key: "sk_live_..."

---
# configmap.yaml (ikke-sensitive konfiguration)
apiVersion: v1
kind: ConfigMap
metadata:
  name: mbus-config
  namespace: mbus-production
data:
  ENVIRONMENT: "production"
  LOG_LEVEL: "INFO"
  MQTT_HOST: "mosquitto-svc"
  MQTT_PORT: "8883"

Horizontal Pod Autoscaler

# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: mbus-server-hpa
  namespace: mbus-production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: mbus-server
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70  # Scale up ved 70% CPU
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

Helm chart struktur

mbus-chart/
├── Chart.yaml
├── values.yaml
├── values-production.yaml
├── values-staging.yaml
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── configmap.yaml
    ├── hpa.yaml
    └── _helpers.tpl

# values.yaml (defaults):
replicaCount: 2
image:
  repository: ghcr.io/kirken20/mbus-gateway/server
  tag: latest
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  host: api.mbus-gateway.dk

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 1000m
    memory: 1Gi

# Deploy:
helm upgrade --install mbus-production ./mbus-chart \
  -f values-production.yaml \
  --namespace mbus-production \
  --create-namespace

Migration fra Docker Compose: Trin-for-trin

Trin 1: Containerisér korrekt (gøres i Docker Compose fase)
  → Non-root user i Dockerfile ✓
  → Health checks ✓
  → Miljøvariabler fra env (ikke hardkodet) ✓
  → Stateless application (sessions i Redis, ikke memory) ✓

Trin 2: Kubernetes lokal (minikube/kind)
  → Konvertér docker-compose.yml til K8s manifests
  → Brug kompose-tool: `kompose convert`
  → Test lokalt inden skyen

Trin 3: Managed Kubernetes (anbefalet)
  → Hetzner Kubernetes Engine (HKE): Billigst i EU
  → Eller DigitalOcean Kubernetes, Scaleway Kapsule
  → IKKE AWS EKS/GKE: For dyrt til SaaS i bootstrapping-fase

Trin 4: Database migration
  → PostgreSQL i K8s: CloudNativePG operator (anbefalet)
  → ELLER: Managed database (Hetzner Managed Database) + K8s app
  → Sidstnævnte: Enklere men dyrere

Trin 5: Argo CD eller Flux til GitOps
  → Git = kilde til sandhed for K8s manifests
  → Push til git → automatisk deploy → K8s

Konklusion

Docker Compose er det rigtige valg for de fleste SaaS-startups under 100 tenants — simpler, billigere og lettere at debugge. Kubernetes giver merværdi ved auto-scaling, zero-downtime rolling updates og multi-region deployment. Migrér når kompleksiteten i Compose overstiger gevinsten. Managed K8s (Hetzner HKE) er den billigste vej til produktion.

Se Hetzner VPS Docker SaaS opsætning eller GitHub Actions Docker deploy guide.