GuíasGuides · Nivel 4Level 4

Kubernetes: de Docker Compose a un clúster gestionado, un despliegue y un pod en CrashLoopBackOffKubernetes: from Docker Compose to a managed cluster, a deployment and a pod in CrashLoopBackOff

Cuándo compensa el salto desde Compose en un servidor y cuándo no, un clúster AKS o EKS, una herramienta interna con Deployment, Service y Gateway API, configuración y secretos, peticiones y límites de recursos, una actualización progresiva con vuelta atrás y cómo leer un pod que no arranca.When the jump from Compose on one server pays off and when it doesn't, an AKS or EKS cluster, an internal tool with Deployment, Service and Gateway API, configuration and secrets, resource requests and limits, a rolling update with rollback, and how to read a pod that won't start.

Qué vas a conseguir

Decidir si una aplicación interna debe pasar de Docker Compose a Kubernetes y, si pasa, desplegarla bien en un clúster gestionado: varias réplicas, configuración fuera de la imagen, recursos acotados, actualizaciones sin corte y la vuelta atrás en un comando. Y leer el problema más típico, un pod que se reinicia sin parar, en cinco minutos.

Antes de empezar

  • La guía de Docker del Nivel 3: imágenes, registro y Compose.
  • La imagen de la aplicación en un registro privado (aquí acrcontoso.azurecr.io/inventario), con etiquetas de versión, nunca solo latest.
  • kubectl y la CLI del proveedor (az o aws), con acceso por tu identidad de Entra ID o IAM Identity Center.
  • Un responsable del clúster en el equipo: es una plataforma que hay que mantener.

1. ¿Compensa el salto?

SituaciónRecomendación
Una o dos herramientas internas, un servidor, se toleran unos minutos de corte al actualizarSe queda en Compose. Copias, monitorización y un docker compose pull && up -d en ventana
Varios servicios, varios equipos que despliegan, se necesita alta disponibilidadKubernetes gestionado (AKS, EKS)
Muchas aplicaciones, requisitos de seguridad fuertes, se quiere un portal para desarrolladoresRed Hat OpenShift o similar, con un equipo de plataforma
Una sola web o API sin estado, sin ganas de gestionar nodosUn servicio de contenedores sin clúster (Azure Container Apps, AWS App Runner o ECS con Fargate)

Lo que da Kubernetes: réplicas que se reponen solas, actualizaciones progresivas y configuración declarativa en Git. Lo que cuesta: actualizar el clúster cada pocos meses, aprender otra herramienta y vigilar más piezas.

2. El clúster gestionado

En AKS, lo que importa al crear el clúster, mejor desde la guía de Terraform que a mano:

  • --tier standard (SLA del plano de control), nodos en tres zonas de disponibilidad, red Azure CNI Overlay y --attach-acr acrcontoso para leer el registro sin secretos.
  • Integración con Entra ID y Azure RBAC para Kubernetes, y --disable-local-accounts: nadie entra con un certificado de administrador compartido.
  • La entrada HTTP: el complemento de enrutamiento de aplicaciones con Gateway API (--enable-gateway-api --enable-app-routing-istio). El proyecto ingress-nginx se retiró en marzo de 2026 y Microsoft da parches para su versión gestionada hasta noviembre de 2026; un clúster nuevo empieza ya con Gateway API, el sucesor del recurso Ingress.

En EKS el equivalente es un clúster con nodos gestionados en tres zonas, acceso con EKS access entries ligadas a IAM Identity Center y el AWS Load Balancer Controller para la entrada.

3. La herramienta interna: Deployment, Service y Gateway

Un espacio de nombres por aplicación (herramientas) y cuatro objetos:

inventario.yaml (extracto)
cat inventario.yamlapiVersion: apps/v1kind: Deploymentmetadata:  name: inventario  namespace: herramientasspec:  replicas: 2  strategy:    rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }  selector:    matchLabels: { app: inventario }  template:    metadata:      labels: { app: inventario }    spec:      containers:        - name: inventario          image: acrcontoso.azurecr.io/inventario:1.4.2          envFrom:            - configMapRef: { name: inventario-config }            - secretRef: { name: inventario-db }          resources:            requests: { cpu: 250m, memory: 256Mi }            limits: { memory: 512Mi }          readinessProbe:            httpGet: { path: /healthz, port: 8080 }---apiVersion: v1kind: Servicemetadata: { name: inventario, namespace: herramientas }spec:  selector: { app: inventario }  ports: [{ port: 80, targetPort: 8080 }]---apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: { name: gw-herramientas, namespace: herramientas }spec:  gatewayClassName: approuting-istio  infrastructure:    annotations:      service.beta.kubernetes.io/azure-load-balancer-internal: "true"  listeners: [{ name: http, port: 80, protocol: HTTP }]---apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: { name: inventario, namespace: herramientas }spec:  parentRefs: [{ name: gw-herramientas }]  hostnames: ["inventario.intranet.contoso.com"]  rules: [{ backendRefs: [{ name: inventario, port: 80 }] }]
  • Deployment: cuántas réplicas, qué imagen y cómo se actualiza. maxUnavailable: 0 significa que nunca baja de dos réplicas sanas durante una actualización.
  • Service: un nombre y una IP estables dentro del clúster delante de pods que vienen y van.
  • Gateway + HTTPRoute: la entrada HTTP. La anotación crea un balanceador interno: la herramienta solo se ve desde la red de la empresa.
  • readinessProbe: el pod no recibe tráfico hasta que /healthz responde.

4. Configuración, secretos y recursos

  • ConfigMap para lo que no es secreto (DB_HOST, nivel de log), en el mismo repositorio que el manifiesto.
  • Secret para la contraseña de la base de datos. Un Secret está en base64, no cifrado para quien pueda leerlo. Lo correcto es guardarlo en Key Vault o Secrets Manager y montarlo con el Secrets Store CSI Driver (en AKS, el complemento Azure Key Vault provider) y una identidad de carga de trabajo. Nunca en el repositorio.
  • requests: lo que el planificador reserva en el nodo. Sin ellas, los pods acaban donde no caben.
  • limits de memoria: si el contenedor los supera, se mata con OOMKilled. Para la CPU se suele poner solo la petición: un límite de CPU frena la aplicación sin avisar.

5. Actualizar y volver atrás

kubectl · actualización a 1.5.0
kubectl -n herramientas set image deployment/inventario inventario=acrcontoso.azurecr.io/inventario:1.5.0deployment.apps/inventario image updatedkubectl -n herramientas rollout status deployment/inventario --timeout=3mWaiting for deployment "inventario" rollout to finish: 1 out of 2 new replicas have been updated...error: timed out waiting for the conditionkubectl -n herramientas get podsNAME                          READY   STATUS             RESTARTS      AGEinventario-5d8b7f6c49-kq7zt   1/1     Running            0             6dinventario-5d8b7f6c49-w2m9r   1/1     Running            0             6dinventario-7c9f8d6b5-x2k4p    0/1     CrashLoopBackOff   5 (40s ago)   4mkubectl -n herramientas rollout undo deployment/inventariodeployment.apps/inventario rolled back

Gracias a maxUnavailable: 0 y a la sonda de disponibilidad, los dos pods de la 1.4.2 siguieron atendiendo: los usuarios no notaron nada. rollout undo vuelve a la revisión anterior; rollout history las lista. En el día a día la imagen se cambia desde el pipeline o con GitOps (Argo CD, Flux).

6. El ticket: "el pod está en CrashLoopBackOff"

CrashLoopBackOff no es la causa: el contenedor arranca, muere y Kubernetes lo reintenta cada vez más tarde. La causa está en tres sitios:

kubectl · diagnóstico
kubectl -n herramientas describe pod inventario-7c9f8d6b5-x2k4p    State:          Waiting      Reason:       CrashLoopBackOff    Last State:     Terminated      Reason:       Error      Exit Code:    1    Restart Count:  5Events:  Type     Reason   Age                 From     Message  ----     ------   ----                ----     -------  Warning  BackOff  15s (x18 over 4m)   kubelet  Back-off restarting failed container inventario in pod inventario-7c9f8d6b5-x2k4pkubectl -n herramientas logs inventario-7c9f8d6b5-x2k4p --previous2026-10-28T09:41:07Z INFO  inventario 1.5.0 arrancando2026-10-28T09:41:07Z FATAL falta la variable DATABASE_URL; desde 1.5.0 sustituye a DB_HOST y DB_NAMEkubectl -n herramientas get events --sort-by=.lastTimestamp | tail -5
  • describe: el motivo y el código de salida del último intento, y los eventos del pod.
  • logs --previous: lo que escribió el contenedor anterior, el que murió. Sin --previous ves el intento actual, que a menudo aún no ha escrito nada.
  • get events: lo que pasa alrededor (nodo sin recursos, volumen que no monta, imagen que no baja).
Lo que vesQué suele ser
Error, código 1La aplicación falla al arrancar: configuración, una variable, la base de datos. Lo dicen los logs
OOMKilled, código 137Superó el límite de memoria. Subir el límite o revisar la aplicación
CreateContainerConfigErrorFalta el ConfigMap o el Secret que referencia el pod
ImagePullBackOffOtra familia: la etiqueta no existe o el clúster no puede leer el registro

Aquí la versión 1.5.0 cambió el nombre de una variable y nadie actualizó el ConfigMap. Se añade DATABASE_URL (en el Secret, porque lleva la contraseña), se revisa en pruebas y se repite la actualización.

7. Lo que revisas cada mes

  • Versión del clúster frente al calendario de soporte de AKS o EKS: la siguiente actualización tiene fecha, primero en pruebas.
  • Pods con reinicios en los últimos 30 días y pods sin requests o sin sondas.
  • Imágenes con vulnerabilidades críticas en el registro (Defender for Containers, Amazon Inspector).

Siguientes pasos

  • La guía de Red Hat OpenShift si lo que se necesita es una plataforma completa con portal, compilaciones y políticas.
  • La guía de Terraform para que el clúster, el registro y la red salgan de código revisado.
  • La guía de Azure Monitor y Log Analytics para Container Insights y alertas sobre reinicios, y la de Grafana para cuadros de mando con Prometheus.
  • La guía de Amazon AWS si el clúster va a ser EKS.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

What you'll get

Deciding whether an internal application should move from Docker Compose to Kubernetes and, if it does, deploying it properly on a managed cluster: several replicas, configuration outside the image, bounded resources, updates without downtime and rollback in one command. And reading the most typical problem, a pod that keeps restarting, in five minutes.

Before you start

  • The Level 3 Docker guide: images, registry and Compose.
  • The application image in a private registry (acrcontoso.azurecr.io/inventory here), with version tags, never just latest.
  • kubectl and the provider CLI (az or aws), with access through your Entra ID or IAM Identity Center identity.
  • A cluster owner on the team: it is a platform that needs maintaining.

1. Is the jump worth it?

SituationRecommendation
One or two internal tools, one server, a few minutes of downtime when updating is acceptableStay on Compose. Backups, monitoring and a docker compose pull && up -d in a window
Several services, several teams deploying, high availability neededManaged Kubernetes (AKS, EKS)
Many applications, strong security requirements, a developer portal wantedRed Hat OpenShift or similar, with a platform team
A single stateless website or API, no appetite for managing nodesA container service without a cluster (Azure Container Apps, AWS App Runner or ECS with Fargate)

What Kubernetes gives you: self-healing replicas, rolling updates and declarative configuration in Git. What it costs: upgrading the cluster every few months, learning another tool and watching more moving parts.

2. The managed cluster

On AKS, what matters when creating the cluster, better from the Terraform guide than by hand:

  • --tier standard (control plane SLA), nodes across three availability zones, Azure CNI Overlay networking and --attach-acr acrcontoso to read the registry without secrets.
  • Entra ID integration and Azure RBAC for Kubernetes, and --disable-local-accounts: nobody gets in with a shared admin certificate.
  • HTTP ingress: the application routing add-on with Gateway API (--enable-gateway-api --enable-app-routing-istio). The ingress-nginx project was retired in March 2026 and Microsoft provides patches for its managed version until November 2026; a new cluster starts straight away with Gateway API, the successor to the Ingress resource.

On EKS the equivalent is a cluster with managed node groups across three zones, access through EKS access entries tied to IAM Identity Center, and the AWS Load Balancer Controller for ingress.

3. The internal tool: Deployment, Service and Gateway

One namespace per application (tools) and four objects:

inventory.yaml (excerpt)
cat inventory.yamlapiVersion: apps/v1kind: Deploymentmetadata:  name: inventory  namespace: toolsspec:  replicas: 2  strategy:    rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }  selector:    matchLabels: { app: inventory }  template:    metadata:      labels: { app: inventory }    spec:      containers:        - name: inventory          image: acrcontoso.azurecr.io/inventory:1.4.2          envFrom:            - configMapRef: { name: inventory-config }            - secretRef: { name: inventory-db }          resources:            requests: { cpu: 250m, memory: 256Mi }            limits: { memory: 512Mi }          readinessProbe:            httpGet: { path: /healthz, port: 8080 }---apiVersion: v1kind: Servicemetadata: { name: inventory, namespace: tools }spec:  selector: { app: inventory }  ports: [{ port: 80, targetPort: 8080 }]---apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: { name: gw-tools, namespace: tools }spec:  gatewayClassName: approuting-istio  infrastructure:    annotations:      service.beta.kubernetes.io/azure-load-balancer-internal: "true"  listeners: [{ name: http, port: 80, protocol: HTTP }]---apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: { name: inventory, namespace: tools }spec:  parentRefs: [{ name: gw-tools }]  hostnames: ["inventory.intranet.contoso.com"]  rules: [{ backendRefs: [{ name: inventory, port: 80 }] }]
  • Deployment: how many replicas, which image and how it is updated. maxUnavailable: 0 means it never drops below two healthy replicas during an update.
  • Service: a stable name and IP inside the cluster in front of pods that come and go.
  • Gateway + HTTPRoute: HTTP ingress. The annotation creates an internal load balancer: the tool is only visible from the company network.
  • readinessProbe: the pod receives no traffic until /healthz answers.

4. Configuration, secrets and resources

  • ConfigMap for what isn't secret (DB_HOST, log level), in the same repository as the manifest.
  • Secret for the database password. A Secret is base64, not encrypted for anyone who can read it. The right way is to keep it in Key Vault or Secrets Manager and mount it with the Secrets Store CSI Driver (on AKS, the Azure Key Vault provider add-on) and a workload identity. Never in the repository.
  • requests: what the scheduler reserves on the node. Without them, pods end up where they don't fit.
  • Memory limits: if the container exceeds them, it is killed with OOMKilled. For CPU usually only the request is set: a CPU limit throttles the application silently.

5. Updating and rolling back

kubectl · update to 1.5.0
kubectl -n tools set image deployment/inventory inventory=acrcontoso.azurecr.io/inventory:1.5.0deployment.apps/inventory image updatedkubectl -n tools rollout status deployment/inventory --timeout=3mWaiting for deployment "inventory" rollout to finish: 1 out of 2 new replicas have been updated...error: timed out waiting for the conditionkubectl -n tools get podsNAME                         READY   STATUS             RESTARTS      AGEinventory-5d8b7f6c49-kq7zt   1/1     Running            0             6dinventory-5d8b7f6c49-w2m9r   1/1     Running            0             6dinventory-7c9f8d6b5-x2k4p    0/1     CrashLoopBackOff   5 (40s ago)   4mkubectl -n tools rollout undo deployment/inventorydeployment.apps/inventory rolled back

Thanks to maxUnavailable: 0 and the readiness probe, the two 1.4.2 pods kept serving: users noticed nothing. rollout undo goes back to the previous revision; rollout history lists them. Day to day the image is changed from the pipeline or with GitOps (Argo CD, Flux).

6. The ticket: "the pod is in CrashLoopBackOff"

CrashLoopBackOff is not the cause: the container starts, dies and Kubernetes retries it later each time. The cause is in three places:

kubectl · diagnosis
kubectl -n tools describe pod inventory-7c9f8d6b5-x2k4p    State:          Waiting      Reason:       CrashLoopBackOff    Last State:     Terminated      Reason:       Error      Exit Code:    1    Restart Count:  5Events:  Type     Reason   Age                 From     Message  ----     ------   ----                ----     -------  Warning  BackOff  15s (x18 over 4m)   kubelet  Back-off restarting failed container inventory in pod inventory-7c9f8d6b5-x2k4pkubectl -n tools logs inventory-7c9f8d6b5-x2k4p --previous2026-10-28T09:41:07Z INFO  inventory 1.5.0 starting2026-10-28T09:41:07Z FATAL missing variable DATABASE_URL; since 1.5.0 it replaces DB_HOST and DB_NAMEkubectl -n tools get events --sort-by=.lastTimestamp | tail -5
  • describe: the reason and exit code of the last attempt, and the pod's events.
  • logs --previous: what the previous container wrote, the one that died. Without --previous you see the current attempt, which often hasn't written anything yet.
  • get events: what is happening around it (node short of resources, volume that won't mount, image that won't pull).
What you seeWhat it usually is
Error, code 1The application fails at startup: configuration, a variable, the database. The logs say which
OOMKilled, code 137It exceeded the memory limit. Raise the limit or look at the application
CreateContainerConfigErrorThe ConfigMap or Secret the pod references is missing
ImagePullBackOffA different family: the tag doesn't exist or the cluster can't read the registry

Here version 1.5.0 renamed a variable and nobody updated the ConfigMap. DATABASE_URL is added (in the Secret, as it carries the password), checked in test and the update repeated.

7. What you review each month

  • Cluster version against the AKS or EKS support calendar: the next upgrade has a date, test first.
  • Pods with restarts in the last 30 days and pods without requests or without probes.
  • Images with critical vulnerabilities in the registry (Defender for Containers, Amazon Inspector).

Next steps

  • The Red Hat OpenShift guide if what you need is a complete platform with a portal, builds and policies.
  • The Terraform guide so that the cluster, registry and network come out of reviewed code.
  • The Azure Monitor y Log Analytics guide for Container Insights and alerts on restarts, and the Grafana guide for dashboards with Prometheus.
  • The Amazon AWS guide if the cluster is going to be EKS.

Is something missing, or different in your environment? Get in touch and I'll fix it.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →