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
- Docker + Docker Compose installés (pour comprendre l’existant)
- Un cluster Kubernetes accessible :
- local : kind, minikube, k3d
- distant : EKS/GKE/AKS, ou cluster on-prem
kubectlconfiguré- (Optionnel mais recommandé)
helm,kustomize,stern,kubectx/kubens
Vérifications rapides :
kubectl version --client
kubectl cluster-info
kubectl get nodes
Objectifs de la migration
- Reproduire le comportement de
docker-compose updans Kubernetes. - Remplacer les concepts Compose (services, volumes, networks, depends_on, env_file) par les primitives K8s.
- 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
- Compose est orienté conteneurs sur une machine (ou Swarm).
- Kubernetes est orienté Pods (unité de scheduling) et déclaratif.
- Les “liens” entre services (réseau, discovery) se font via Services et DNS interne.
- La persistance se fait via PersistentVolumeClaim (PVC) + StorageClass.
- Les dépendances de démarrage (
depends_on) ne sont pas garanties : on utilise des readiness probes, des initContainers, et une logique applicative robuste.
3) Stratégie de migration : étapes recommandées
- Construire et publier les images (registry).
- Créer un namespace dédié.
- Déployer la base (Postgres) avec PVC.
- Déployer Redis.
- Déployer l’API (avec variables d’environnement, probes).
- Déployer le Web.
- Exposer via Ingress (ou NodePort/LoadBalancer selon contexte).
- 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)
services:→ Deployment (ou StatefulSet) + Serviceports:→ Service (ClusterIP) + éventuellement Ingress/LoadBalancerenvironment:→ env (Deployment) / ConfigMap / Secretdepends_on:→ readinessProbe, initContainers, retries applicatifsvolumes:→ PersistentVolumeClaim (PVC) + VolumeMountsnetworks:→ réseau plat K8s + DNS (service.namespace.svc.cluster.local)
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: Nonecré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_URLdirectement ? 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 :
- l’API peut démarrer avant Postgres/Redis
- mais elle ne doit être Ready que quand elle peut servir du trafic
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 :
- implémenter un endpoint health “dégradé” (liveness vs readiness distincts)
- ou ajouter un
initContainerqui attend Postgres/Redis
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
- ConfigMap : paramètres non sensibles (URLs internes, flags, niveaux de logs).
- Secret : mots de passe, tokens, clés API (attention : encodage base64 ≠ chiffrement).
- Évitez de multiplier les variables : préférez un fichier de config monté en volume si l’app le supporte.
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 :
- un PVC demande du stockage
- un StorageClass provisionne dynamiquement (si disponible)
- selon le backend (EBS, Ceph, NFS…), les contraintes changent (ReadWriteOnce, ReadWriteMany…)
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”
- Images versionnées et scannées (Trivy, Grype).
- Probes : readiness + liveness (et startupProbe si besoin).
- Ressources requests/limits définies.
- Secrets gérés proprement (External Secrets Operator, Vault, SOPS… si possible).
- Persistance : PVC + stratégie backup/restore.
- Ingress + TLS (cert-manager).
- Observabilité : logs centralisés, métriques, traces.
- NetworkPolicies (si exigence sécurité).
- 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
- Scalabilité horizontale (replicas, HPA)
- Résilience (self-healing, rescheduling)
- Déploiements progressifs (rolling updates, rollback)
- Découplage infra/app (PVC, Ingress, Service discovery)
- Standardisation (manifests, Helm, GitOps)
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.