Compose y Kubernetes pueden ejecutar los mismos tres procesos: una UI web, una API, y Postgres. Compose es un proyecto en un motor. Kubernetes es un conjunto de objetos API que un plano de control puede reprogramar. Abajo hay un compose.yaml, los manifiestos que coinciden con él, y el punto en que un equipo debe cambiar de herramientas.
Respuesta corta
Elige Compose cuando perder la máquina significa que la app está abajo, y eso es aceptable. Elige Kubernetes cuando un nodo puede desaparecer y el Pod debe iniciarse en otro lugar, o cuando ya operabas un clúster. Ambas herramientas inician los contenedores web, API, y base de datos. Solo Kubernetes tiene un planificador, un Service que rastrea Pods listos, e historial de despliegue.
Los nombres de servicio en Compose son nombres DNS en la red del proyecto. Los nombres de Service en Kubernetes son nombres DNS dentro del namespace (api y api.demo.svc.cluster.local). http://api:3000 funciona en ambos archivos si el Service se llama api y escucha en puerto 3000.
Lado a lado
| Preocupación | compose.yaml | Kubernetes |
|---|---|---|
| Proceso web, API, base de datos | services.web, services.api, services.postgres |
Deployment + Pod template |
| Puerto UI publicado | ports: ["8080:80"] en web |
ClusterIP Service, luego kubectl port-forward o un Ingress |
| Dirección de API | http://api:3000 |
Service api puerto 3000 |
| Configuración sin secreto | environment |
ConfigMap app-config |
| Contraseña | variable .env |
Secret app-secret |
| Disco de base de datos | volumen nombrado pgdata |
PersistentVolumeClaim pgdata |
| Orden de inicio | depends_on con service_healthy |
readinessProbe; clientes reintentan |
| Health check | healthcheck dentro del contenedor |
readinessProbe y livenessProbe, ejecutados por el kubelet |
| Reinicio | restart: unless-stopped |
Deployment recrea el Pod (restartPolicy: Always) |
| Copias extra de la API | docker compose up --scale api=3 |
spec.replicas |
| Compilación | docker build, luego etiqueta de imagen |
misma referencia de imagen; el clúster no construye |
El archivo Compose
change-me es un marcador para una contraseña. Ponlo en un archivo .env junto al archivo Compose y deja ese archivo fuera de git. Compose lee .env automáticamente. Los detalles del comando están en la chuleta de Docker Compose.
services:
web:
image: example/web:1.4.0
ports:
- "8080:80"
environment:
API_URL: http://api:3000
depends_on:
api:
condition: service_healthy
restart: unless-stopped
api:
image: example/api:1.4.0
environment:
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@postgres:5432/app
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
restart: unless-stopped
postgres:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
volumes:
pgdata:
El check wget se ejecuta dentro de la imagen, así que el binario tiene que existir allí. Construye etiquetas con la chuleta de Dockerfile.
# --wait devuelve non-zero si un healthcheck nunca pasa
docker compose up -d --wait
docker compose ps
docker compose down -v
down -v elimina el volumen pgdata. Úsalo en CI. No lo uses contra una base de datos que quisiste mantener.
Los manifiestos de Kubernetes
Guarda esto como shop.yaml y aplícalo con kubectl. El valor de Secret debe coincidir con POSTGRES_PASSWORD o la API no puede iniciar sesión. En un clúster compartido, crea ese Secret fuera del repo de git y elimina el bloque stringData.
apiVersion: v1
kind: Namespace
metadata:
name: demo
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: demo
data:
API_URL: "http://api:3000"
POSTGRES_USER: app
POSTGRES_DB: app
---
apiVersion: v1
kind: Secret
metadata:
name: app-secret
namespace: demo
type: Opaque
stringData:
POSTGRES_PASSWORD: change-me
DATABASE_URL: postgres://app:change-me@postgres:5432/app
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pgdata
namespace: demo
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
securityContext:
fsGroup: 999
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
valueFrom:
configMapKeyRef: {name: app-config, key: POSTGRES_USER}
- name: POSTGRES_DB
valueFrom:
configMapKeyRef: {name: app-config, key: POSTGRES_DB}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef: {name: app-secret, key: POSTGRES_PASSWORD}
readinessProbe:
exec:
command: ["pg_isready", "-U", "app", "-d", "app"]
periodSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-U", "app", "-d", "app"]
initialDelaySeconds: 15
periodSeconds: 10
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: pgdata
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: demo
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: example/api:1.4.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef: {name: app-secret, key: DATABASE_URL}
readinessProbe:
httpGet: {path: /health, port: 3000}
periodSeconds: 10
resources:
requests: {cpu: "100m", memory: 128Mi}
limits: {memory: 256Mi}
---
apiVersion: v1
kind: Service
metadata:
name: api
namespace: demo
spec:
selector:
app: api
ports:
- port: 3000
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: example/web:1.4.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
env:
- name: API_URL
valueFrom:
configMapKeyRef: {name: app-config, key: API_URL}
readinessProbe:
httpGet: {path: /, port: 80}
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: demo
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80
Los probes httpGet se ejecutan en el kubelet, así que esas imágenes no necesitan wget. fsGroup: 999 permite que la imagen oficial de Postgres escriba su volumen. imagePullPolicy: IfNotPresent encaja con una etiqueta cargada localmente; :latest aún usa Always por defecto y contacta el registro.
kubectl apply -f shop.yaml
kubectl rollout status deployment/api -n demo
kubectl port-forward -n demo svc/web 8080:80
port-forward reemplaza el 8080:80 de Compose en tu laptop. Otras máquinas deben usar un Ingress.
Un PVC sin StorageClass se queda Pending (Pod Pending / FailedScheduling). Un límite que es muy pequeño es OOMKilled. Un DATABASE_URL malo es CrashLoopBackOff. Una imagen faltante es ImagePullBackOff.
Deja Postgres en una réplica. ReadWriteOnce no montará el mismo disco en dos nodos. Usa un StatefulSet y volumeClaimTemplates más tarde si necesitas un nombre estable y un volumen por réplica.
Cuándo elegir Compose
Elige Compose cuando todo esto es verdadero:
- La pila cabe en una máquina, incluido disco para Postgres.
- Una política de reinicio es suficiente cuando un proceso sale.
- Desarrolladores y CI ya ejecutan
docker compose up. - Un Pod no tiene que moverse a otro nodo.
Mantén Compose para el bucle interior incluso después de que la producción es un clúster. Un cambio de archivo y docker compose up es más corto que un push y un rollout.
Cuándo elegir Kubernetes
Elige Kubernetes cuando uno de estos es verdadero:
- Necesitas más de un nodo, o un nodo puede drenarse mientras la API sigue sirviendo.
- Quieres un rollout grabado y
kubectl rollout undopor Deployment. - Varios entornos deben compartir los mismos manifiestos, con pequeñas diferencias manejadas por Helm o Kustomize.
- El equipo de plataforma ya ejecuta el clúster y espera requests, límites, y probes.
Una segunda réplica no es razón suficiente. --scale ya inicia contenedores API extra en un motor. Muévete cuando la colocación, actualizaciones, o política importan.
Camino intermedio
Compose en CI. Inicia la pila, falla el trabajo si los healthchecks no pasan, luego elimina volúmenes:
docker compose up -d --wait
docker compose ps
docker compose down -v
kind para un plano de control local. Carga las imágenes web y API, luego aplica shop.yaml. El nodo aún puede extraer postgres:16. Cuál clúster local usar se cubre en kind vs minikube vs k3d.
kind create cluster --name demo
kind load docker-image example/web:1.4.0 --name demo
kind load docker-image example/api:1.4.0 --name demo
kubectl apply -f shop.yaml
Kompose como borrador, no como pipeline. Del directorio que contiene compose.yaml:
kompose convert -f compose.yaml
Kompose escribe un archivo YAML por objeto en el directorio actual. Revisa probes, la contraseña, imagePullPolicy, y el tipo de Service antes de que alguien los aplique. Kubernetes no ejecuta compose.yaml por sí solo.
Ruta de migración
- Fija etiquetas de imagen en CI. El clúster debe referenciar
example/api:1.4.0, no unlatestflotante ni un bloquebuild:. - División de configuración. Non-secretos van al ConfigMap. La contraseña va a un Secret creado fuera de git en cualquier clúster compartido.
- Convierte cada healthcheck de Compose en un readinessProbe. Mantén un livenessProbe solo para un check que significa «reinicia este contenedor.»
- Crea el PVC y copia datos con tu propio backup.
docker compose down -vmás un claim fresco deja caer la base de datos. - Aplica
shop.yamlen kind. Corrige Pods Pending e image pulls allí. - Añade requests y límites, y mira la API bajo carga antes de copiar los números.
- Pon un Ingress delante de web. Deja
kubectl port-forwardpara debuggear. - Decide dónde vive Postgres antes de que usuarios se muevan: una base de datos manejada, o un StatefulSet con backups.
- Aplica en el clúster compartido, ejecuta
kubectl rollout status, luego cambia DNS.
Preguntas frecuentes
¿Es Docker Compose lo mismo que Kubernetes?
No. Compose inicia contenedores en un motor Docker y los conecta con una red de proyecto. Kubernetes almacena Deployments, Services, y volúmenes en una API y puede colocar Pods en otros nodos. La imagen puede ser idéntica. El modelo de objetos no.
¿Cuándo debo cambiar de Docker Compose a Kubernetes?
Cambia cuando el proceso tiene que sobrevivir a un fallo de nodo, desplegar sin tiempo de inactividad, o compartir un clúster que otros equipos ya ejecutan. Mantente en Compose cuando una máquina, una política de reinicio, y un trabajo de CI son suficientes. Una diapositiva que dice producción no es un requisito para Kubernetes.
¿Kompose produce manifiestos listos para producción?
Produce un primer borrador. kompose convert -f compose.yaml escribe un Deployment y un Service por cada servicio de Compose, y un PersistentVolumeClaim para volúmenes nombrados. Todavía editas probes, Secrets, límites de recursos, y el tipo de Service antes de que alguien más aplique los archivos.
¿Qué reemplaza depends_on en Kubernetes?
Nada espera una vez al inicio y luego se aparta. Un readinessProbe elimina un Pod de los endpoints del Service hasta que puede servir, y sigue haciendo eso de por vida del Pod. Los clientes deben reintentar. Un init container puede bloquear el app container, pero no reemplaza el probe.
Cómo se verificó esta guía
Escrita a mano a partir de la documentación oficial, con cada comando comprobado de nuevo y una segunda revisión antes de publicar. Última revisión: .
Fuentes oficiales consultadas para este artículo

