Szymon Borowski
Extended\Mind::Thesis()
The mind extends beyond the skull — into tools, notes, and environment. — Clark & Chalmers, 1998

From Docker Compose to Kubernetes — my step-by-step migration

Szymon Borowski ·

Starting point — Docker Compose

Docker Compose worked great in development. One file, docker compose up and everything runs. But Compose doesn't provide:

  • automatic restart after a node failure
  • rolling updates without downtime
  • horizontal scaling
  • declarative configuration management

Kubernetes solves all of these problems.

Mapping concepts

Docker Compose Kubernetes
service Deployment + Service
volumes PersistentVolumeClaim
environment ConfigMap + Secret
networks NetworkPolicy
depends_on initContainer
healthcheck livenessProbe + readinessProbe

Deployment manifest

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    spec:
      containers:
        - name: frontend
          image: ghcr.io/szymonborowski/portfolio-frontend:v0.0.4
          ports:
            - containerPort: 80
          envFrom:
            - configMapRef:
                name: frontend-config
            - secretRef:
                name: frontend-secrets
          livenessProbe:
            httpGet:
              path: /health
              port: 80
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 5

InitContainers — database migrations

The biggest problem: how to make sure DB migrations run before the application starts? In Compose I used depends_on with condition: service_healthy. In K8s I use an initContainer:

initContainers:
  - name: migrate
    image: ghcr.io/szymonborowski/portfolio-blog:v0.0.4
    command: ["php", "artisan", "migrate", "--force"]
    envFrom:
      - secretRef:
          name: blog-secrets

The initContainer must complete successfully before the main container starts.

StatefulSets for databases

MySQL and Redis run as a StatefulSet, not a Deployment — because they need a stable network identity and persistent storage:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: blog-db
spec:
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

What went wrong

The biggest pitfall: ARM vs AMD64 images. I was building images on a MacBook (ARM) without --platform linux/amd64. On the cluster (AMD64) containers crashed with an "exec format error". The fix: multi-arch build with docker buildx:

docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/... --push .
Likes
Login — Log in to leave a comment.

Comments

No comments yet