← Retour aux tutoriels

De Docker à Kubernetes : migrer un projet Docker Compose vers K8s (guide pratique DevOps)

dockerdocker-composekubernetesmigrationdevopscontainersci-cdhelmingressobservabilite

De Docker à Kubernetes : migrer un projet Docker Compose vers K8s (guide pratique DevOps)

Migrer d’un projet Docker Compose vers Kubernetes (K8s) est une étape fréquente quand on passe d’un environnement local/monohôte à une plateforme d’orchestration robuste (scalabilité, haute dispo, rolling updates, autoscaling, observabilité, politiques réseau, etc.). Ce guide pratique couvre une migration réaliste, avec des commandes exécutables, des manifests Kubernetes complets, et des explications sur les choix d’architecture.


1) Prérequis et objectifs

Prérequis

Vérifications rapides :

kubectl version --client
kubectl cluster-info
kubectl get nodes

Objectifs de la migration

  1. Reproduire le comportement de docker-compose up dans Kubernetes.
  2. Remplacer les concepts Compose (services, volumes, networks, depends_on, env_file) par les primitives K8s.
  3. Rendre production-ready : healthchecks, ressources, rolling updates, secrets, ingress, persistance.

2) Exemple de projet Docker Compose (point de départ)

Prenons un exemple courant : une application web + API + base PostgreSQL + cache Redis.

docker-compose.yml (extrait représentatif) :

version: "3.9"
services:
  web:
    build: ./web
    ports:
      - "8080:80"
    environment:
      - API_URL=http://api:3000
    depends_on:
      - api

  api:
    build: ./api
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://app:app@db:5432/app
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis

  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=app
      - POSTGRES_DB=app
    volumes:
      - dbdata:/var/lib/postgresql/data

  redis:
    image: redis:7

volumes:
  dbdata:

Comment Kubernetes “pense” différemment


3) Stratégie de migration : étapes recommandées

  1. Construire et publier les images (registry).
  2. Créer un namespace dédié.
  3. Déployer la base (Postgres) avec PVC.
  4. Déployer Redis.
  5. Déployer l’API (avec variables d’environnement, probes).
  6. Déployer le Web.
  7. Exposer via Ingress (ou NodePort/LoadBalancer selon contexte).
  8. Ajouter : ressources, autoscaling, secrets, configmaps, observabilité.

4) Construire les images et les pousser dans un registry

En Kubernetes, le cluster doit pouvoir tirer les images. En local (kind/minikube), on peut charger les images directement, mais en général on pousse sur un registry (Docker Hub, GHCR, GitLab, ECR…).

Exemple avec Docker Hub (adaptez moncompte) :

docker build -t moncompte/web:1.0.0 ./web
docker build -t moncompte/api:1.0.0 ./api

docker login
docker push moncompte/web:1.0.0
docker push moncompte/api:1.0.0

Si vous utilisez kind, vous pouvez charger sans push :

kind load docker-image moncompte/web:1.0.0
kind load docker-image moncompte/api:1.0.0

5) Créer un namespace Kubernetes

Un namespace isole les ressources (pratique pour dev/staging/prod).

kubectl create namespace demo-compose
kubectl config set-context --current --namespace=demo-compose
kubectl get ns

6) Traduire les concepts Compose vers Kubernetes

Tableau de correspondance (mental model)


7) Déployer PostgreSQL (Stateful + persistance)

Pour une base, on préfère souvent un StatefulSet (identité stable, stockage stable). En environnement de production, on utilise souvent un service managé (RDS/Cloud SQL) ou un opérateur (Zalando, Crunchy, etc.). Ici, on fait “simple mais correct”.

7.1 Secret pour les identifiants DB

Évitez de mettre des mots de passe en clair dans les manifests.

kubectl create secret generic postgres-secret \
  --from-literal=POSTGRES_USER=app \
  --from-literal=POSTGRES_PASSWORD=app \
  --from-literal=POSTGRES_DB=app

Vérification :

kubectl get secret postgres-secret
kubectl describe secret postgres-secret

7.2 PVC + StatefulSet + Service (manifest)

Créez postgres.yaml :

apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  selector:
    app: postgres
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432
  clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
              name: postgres
          envFrom:
            - secretRef:
                name: postgres-secret
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          readinessProbe:
            exec:
              command: ["sh", "-c", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            exec:
              command: ["sh", "-c", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
            initialDelaySeconds: 30
            periodSeconds: 10
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: postgres-pvc

Appliquer :

kubectl apply -f postgres.yaml
kubectl get pods
kubectl get pvc
kubectl logs -f statefulset/postgres

Note : clusterIP: None crée un Headless Service, utile pour StatefulSet. Ici, une seule réplique, mais c’est une base saine.


8) Déployer Redis (stateless simple)

redis.yaml :

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  selector:
    app: redis
  ports:
    - name: redis
      port: 6379
      targetPort: 6379
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7
          ports:
            - containerPort: 6379
          readinessProbe:
            tcpSocket:
              port: 6379
            initialDelaySeconds: 5
            periodSeconds: 5
          livenessProbe:
            tcpSocket:
              port: 6379
            initialDelaySeconds: 15
            periodSeconds: 10

Appliquer :

kubectl apply -f redis.yaml
kubectl get deploy,svc,pods -l app=redis

9) Déployer l’API (Deployment + Service + Config)

9.1 ConfigMap pour les variables non sensibles

On met ce qui n’est pas secret dans un ConfigMap.

api-config.yaml :

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config
data:
  REDIS_URL: "redis://redis:6379"
  # On référence postgres via le Service "postgres" (DNS interne)
  DATABASE_HOST: "postgres"
  DATABASE_PORT: "5432"
  DATABASE_NAME: "app"
  DATABASE_USER: "app"

Pourquoi ne pas mettre DATABASE_URL directement ? On peut, mais séparer facilite la gestion, et évite de reconstruire une URL complète dans des environnements différents. À vous de choisir selon votre application.

9.2 Secret pour le mot de passe DB côté API

POSTGRES_PASSWORD est déjà dans postgres-secret. On peut le réutiliser.

9.3 Deployment + Service API

api.yaml :

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api
  ports:
    - name: http
      port: 3000
      targetPort: 3000
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: moncompte/api:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 3000
              name: http
          env:
            - name: REDIS_URL
              valueFrom:
                configMapKeyRef:
                  name: api-config
                  key: REDIS_URL
            - name: DATABASE_HOST
              valueFrom:
                configMapKeyRef:
                  name: api-config
                  key: DATABASE_HOST
            - name: DATABASE_PORT
              valueFrom:
                configMapKeyRef:
                  name: api-config
                  key: DATABASE_PORT
            - name: DATABASE_NAME
              valueFrom:
                configMapKeyRef:
                  name: api-config
                  key: DATABASE_NAME
            - name: DATABASE_USER
              valueFrom:
                configMapKeyRef:
                  name: api-config
                  key: DATABASE_USER
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: POSTGRES_PASSWORD
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 6
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"

Appliquer :

kubectl apply -f api-config.yaml
kubectl apply -f api.yaml
kubectl get deploy,svc,pods -l app=api
kubectl logs -f deploy/api

9.4 Remplacer depends_on

Dans Compose, depends_on gère l’ordre de démarrage (partiellement). Dans Kubernetes :

C’est exactement le rôle de readinessProbe. Tant que /health échoue, le Service n’enverra pas de trafic vers ce Pod.

Si votre API ne peut pas exposer /health sans DB, vous pouvez :

Exemple d’initContainer (optionnel) :

initContainers:
  - name: wait-postgres
    image: busybox:1.36
    command: ["sh", "-c", "until nc -z postgres 5432; do echo waiting for postgres; sleep 2; done"]
  - name: wait-redis
    image: busybox:1.36
    command: ["sh", "-c", "until nc -z redis 6379; do echo waiting for redis; sleep 2; done"]

10) Déployer le Web (frontend) et le relier à l’API

10.1 Service + Deployment Web

web.yaml :

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: 80
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: moncompte/web:1.0.0
          ports:
            - containerPort: 80
              name: http
          env:
            - name: API_URL
              value: "http://api:3000"
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 5
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 20
            periodSeconds: 10
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "300m"
              memory: "256Mi"

Appliquer :

kubectl apply -f web.yaml
kubectl get deploy,svc,pods -l app=web

11) Exposer l’application : port-forward, NodePort, Ingress

11.1 Test rapide avec kubectl port-forward

Idéal pour valider sans config réseau.

kubectl port-forward svc/web 8080:80
# Puis ouvrir http://localhost:8080

11.2 Ingress (recommandé)

Il faut un contrôleur Ingress (nginx-ingress, traefik…). Sur minikube :

minikube addons enable ingress
kubectl get pods -n ingress-nginx

ingress.yaml (exemple nginx) :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: demo.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

Appliquer :

kubectl apply -f ingress.yaml
kubectl get ingress

En local, ajoutez dans /etc/hosts (selon IP ingress/minikube) :

minikube ip
# puis
sudo sh -c 'echo "192.168.49.2 demo.local" >> /etc/hosts'

Test :

curl -i http://demo.local/

12) Debug et troubleshooting (indispensable en migration)

12.1 Voir l’état et les événements

kubectl get pods -o wide
kubectl describe pod <nom-du-pod>
kubectl get events --sort-by=.metadata.creationTimestamp

12.2 Logs (multi-réplicas)

kubectl logs deploy/api --tail=200 -f
kubectl logs deploy/web --tail=200 -f

Avec stern (pratique) :

stern api
stern web

12.3 Tester la connectivité DNS/service depuis un Pod

Lancez un Pod temporaire :

kubectl run -it --rm debug --image=busybox:1.36 -- sh

Puis :

nslookup api
nslookup postgres
wget -qO- http://api:3000/health
nc -zv postgres 5432
nc -zv redis 6379

13) Gestion de configuration : ConfigMap vs Secret vs fichiers

Bonnes pratiques

Créer un Secret depuis un fichier :

kubectl create secret generic api-secret --from-file=./secrets/api-key.txt

Monter en volume (exemple) :

volumeMounts:
  - name: api-secret-vol
    mountPath: /run/secrets
    readOnly: true
volumes:
  - name: api-secret-vol
    secret:
      secretName: api-secret

14) Persistance : ce qui change vraiment vs Docker volumes

Dans Compose, dbdata: est un volume “local Docker”. En Kubernetes :

Inspecter la StorageClass :

kubectl get storageclass
kubectl describe storageclass <nom>

Inspecter le PVC :

kubectl describe pvc postgres-pvc

15) Rolling updates, disponibilité et scaling

15.1 Scaling manuel

kubectl scale deploy/api --replicas=3
kubectl scale deploy/web --replicas=3
kubectl get pods -l app=api

15.2 Rolling update (changement d’image)

kubectl set image deploy/api api=moncompte/api:1.0.1
kubectl rollout status deploy/api
kubectl rollout history deploy/api

Revenir en arrière :

kubectl rollout undo deploy/api

15.3 HPA (autoscaling CPU)

Nécessite le metrics-server.

Sur minikube :

minikube addons enable metrics-server
kubectl get pods -n kube-system | grep metrics

Créer un HPA :

kubectl autoscale deployment api --cpu-percent=70 --min=2 --max=10
kubectl get hpa

16) Réseau et sécurité : points de vigilance

16.1 Par défaut, tout parle à tout

Dans beaucoup de clusters, sans NetworkPolicies, les Pods peuvent communiquer librement. En production, vous pouvez restreindre.

Exemple (conceptuel) : autoriser l’API à parler à Postgres et Redis, et le Web à parler à l’API. (La mise en œuvre dépend du CNI : Calico, Cilium, etc.)

16.2 Comptes de service et RBAC

Pour une app classique, évitez de donner des permissions Kubernetes inutiles. Par défaut, un Pod n’a pas besoin d’accéder à l’API K8s.


17) “Compose-like” : outils pour accélérer (kompose, helm, kustomize)

17.1 Kompose (conversion automatique)

Kompose peut convertir un docker-compose.yml en manifests K8s. C’est utile pour démarrer, mais rarement “production-ready” sans retouches.

Installation (exemple Linux) :

curl -L https://github.com/kubernetes/kompose/releases/download/v1.34.0/kompose-linux-amd64 -o kompose
chmod +x kompose
sudo mv kompose /usr/local/bin/
kompose version

Conversion :

kompose convert -f docker-compose.yml
ls -la

Puis appliquez et ajustez (probes, ressources, secrets, PVC, ingress).

17.2 Kustomize (overlays dev/staging/prod)

Kustomize est intégré à kubectl :

kubectl kustomize ./k8s
kubectl apply -k ./k8s

17.3 Helm (packaging et templating)

Helm est très pratique pour paramétrer (images, replicas, host ingress) et déployer de manière standardisée.


18) Check-list de migration “prête prod”

  1. Images versionnées et scannées (Trivy, Grype).
  2. Probes : readiness + liveness (et startupProbe si besoin).
  3. Ressources requests/limits définies.
  4. Secrets gérés proprement (External Secrets Operator, Vault, SOPS… si possible).
  5. Persistance : PVC + stratégie backup/restore.
  6. Ingress + TLS (cert-manager).
  7. Observabilité : logs centralisés, métriques, traces.
  8. NetworkPolicies (si exigence sécurité).
  9. CI/CD : déploiement automatisé (GitOps Argo CD/Flux ou pipeline).

19) Commandes récapitulatives (workflow quotidien)

# Appliquer tout
kubectl apply -f postgres.yaml
kubectl apply -f redis.yaml
kubectl apply -f api-config.yaml
kubectl apply -f api.yaml
kubectl apply -f web.yaml
kubectl apply -f ingress.yaml

# Surveiller
kubectl get pods -w
kubectl get svc,ingress
kubectl describe pod <pod>
kubectl logs -f deploy/api

# Tester en local
kubectl port-forward svc/web 8080:80
curl -i http://localhost:8080

20) Conclusion : ce que vous gagnez en passant de Compose à Kubernetes

La migration n’est pas qu’une traduction mécanique : elle implique d’adopter les patterns Kubernetes (probes, déclaratif, séparation config/secrets, persistance via PVC, exposition via Ingress). Une fois cette base en place, vous pouvez industrialiser : Helm/Kustomize, environnements multiples, CI/CD, policies, et observabilité.

Si vous me donnez votre docker-compose.yml réel (et éventuellement vos Dockerfiles), je peux proposer une conversion complète en manifests Kubernetes adaptés (dev/staging/prod) avec Ingress, TLS, ressources, et stratégie de persistance.