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 undo por 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

  1. Fija etiquetas de imagen en CI. El clúster debe referenciar example/api:1.4.0, no un latest flotante ni un bloque build:.
  2. 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.
  3. Convierte cada healthcheck de Compose en un readinessProbe. Mantén un livenessProbe solo para un check que significa «reinicia este contenedor.»
  4. Crea el PVC y copia datos con tu propio backup. docker compose down -v más un claim fresco deja caer la base de datos.
  5. Aplica shop.yaml en kind. Corrige Pods Pending e image pulls allí.
  6. Añade requests y límites, y mira la API bajo carga antes de copiar los números.
  7. Pon un Ingress delante de web. Deja kubectl port-forward para debuggear.
  8. Decide dónde vive Postgres antes de que usuarios se muevan: una base de datos manejada, o un StatefulSet con backups.
  9. 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

Share.

Los comentarios están cerrados.