Qué es Kubernetes y cómo se relaciona con Docker: guía para principiantes

Si ya usas Docker, sabes empaquetar una aplicación en una imagen y arrancarla en un contenedor. Pero en producción surgen otras preguntas: ¿qué pasa si el contenedor se cae a las tres de la mañana?, ¿cómo pongo cinco copias si llega mucho tráfico?, ¿cómo actualizo sin dejar la web sin servicio? Kubernetes responde a todas: es un sistema que gobierna contenedores en uno o muchos equipos. Tú le describes cómo quieres que esté tu aplicación y él se encarga de que esté así, todo el tiempo. Esta guía lo explica desde cero, con esquemas y con un clúster de pruebas real.

El clúster de pruebas. Todo lo que verás se ejecutó en un Kubernetes 1.37.1 de un solo nodo, montado con k3s dentro de un contenedor desechable, con una pequeña app en Python que responde con su versión y el nombre del pod que atiende cada petición. Las imágenes de la app se construyeron con Docker.

Kubernetes en tres ideas

  • Describes, no ordenas. Con Docker dices «arranca este contenedor». Con Kubernetes escribes en un archivo «quiero tres copias de esta imagen, escuchando en este puerto» y se lo entregas.
  • Vigila sin parar. Kubernetes compara continuamente lo que pediste con lo que hay. Si falta una copia, la crea; si sobra, la quita. Por eso se dice que se «repara solo».
  • Junta muchos equipos en uno. A un grupo de máquinas con Kubernetes se le llama clúster. Tú no eliges en qué máquina va cada contenedor: lo decide Kubernetes según el espacio libre.

El nombre viene del griego y significa «timonel». Lo creó Google a partir de su experiencia con Borg, el sistema con el que lleva años gobernando sus propios centros de datos, y lo publicó como código abierto en 2014. Hoy lo mantiene la Cloud Native Computing Foundation y lo ofrecen todas las grandes nubes. Como es una palabra larga, se abrevia K8s: la K, ocho letras y la s.

Docker y Kubernetes no compiten: trabajan juntos

Es la confusión más habitual. Docker y Kubernetes no hacen lo mismo:

  • Docker construye imágenes (docker build) y arranca contenedores en un equipo (docker run).
  • Kubernetes no construye nada: recibe imágenes ya hechas y decide cuántas copias arrancar, en qué equipos, cómo repartir el tráfico entre ellas y cómo sustituirlas cuando hay una versión nueva.

Docker arranca contenedores; Kubernetes los gobierna la imagen es la misma en los dos casos: la construyes con Docker Docker: un equipo docker run -p 8080:8000 saludo:1 tu equipo o un servidor saludo:1 postgres tú decides qué arranca, dónde y cuántas copias Si el contenedor muere o el equipo se apaga, lo arreglas tú. Kubernetes: un clúster de equipos kubectl apply -f saludo.yaml «quiero 3 copias de saludo:1» plano de control API · planificador · controladores y la base de datos del clúster nodo 1 kubelet + containerd saludo saludo nodo 2 kubelet + containerd saludo nodo 3 kubelet + containerd libre para más Tú describes lo que quieres; Kubernetes decide dónde va cada copia, la vigila, la vuelve a crear si falla y reparte el tráfico entre todas.

Esquema: decodigo.com. En la prueba, el clúster tenía un solo nodo; en producción suelen ser varios.

El flujo normal es: escribes tu Dockerfile, construyes la imagen con Docker, la subes a un registro (como Docker Hub o el de tu nube) y Kubernetes la descarga en cada nodo que la necesite. Si ya sabes Docker, tienes medio camino hecho: casi todo tiene su equivalente.

Con DockerCon KubernetesPara qué
docker run saludo:1un Deploymentarrancar la app, aquí con el número de copias que quieras
el contenedorel podlo que corre: uno o varios contenedores que viven juntos
-p 8080:8000un Servicellegar a la app por un nombre y un puerto fijos
--restart alwaysel propio Deploymentvolver a arrancarla si se cae, aquí también si se cae el equipo
-e o env_fileConfigMap y Secretpasar configuración y contraseñas
-v datos:/var/lib/…PersistentVolumeClaimguardar datos que sobrevivan al contenedor
compose.yamllos manifiestos YAMLdescribir la aplicación entera en archivos
docker ps · logs · execkubectl get pods · logs · execver qué corre, leer su salida y entrar en él

¿Kubernetes ya no usa Docker?

Quizá leíste en 2022 que «Kubernetes abandona Docker». Es verdad a medias. Para arrancar contenedores, cada nodo usa un motor de contenedores, y desde la versión 1.24 Kubernetes ya no habla directamente con Docker, sino con motores más ligeros como containerd, que por cierto es el mismo que Docker lleva por dentro. En nuestro clúster de pruebas el motor es containerd. Pero las imágenes que construyes con Docker funcionan igual, porque todas siguen el mismo estándar, el de la Open Container Initiative. Para ti no cambia nada: sigues usando Docker en tu equipo para construir y probar.

¿Lo necesitas?

No siempre. Kubernetes es potente, pero también complejo. Para una web pequeña o un proyecto personal, un servidor con Docker Compose suele bastar, como contamos en la guía de Docker. Kubernetes empieza a compensar cuando tienes varios servidores, varios servicios que se hablan entre sí, necesitas escalar según la demanda o no te puedes permitir cortes al actualizar. Y aunque no lo administres tú, es muy probable que tu empresa o tu nube lo usen por debajo: entender sus piezas te ayudará igual.

Las piezas que hay que conocer

  • Clúster: el conjunto de equipos que gobierna Kubernetes.
  • Nodo: cada uno de esos equipos. En cada nodo corre el kubelet, el agente que arranca los contenedores que le mandan.
  • Plano de control: el cerebro. Recibe tus órdenes por una API, guarda el estado del clúster, decide en qué nodo va cada cosa y vigila que todo cuadre.
  • Pod: la unidad mínima. Casi siempre es un contenedor; a veces, varios que tienen que ir juntos. Cada pod tiene su propia IP, pero es desechable: si muere, se crea otro con otro nombre y otra IP.
  • Deployment: dice qué imagen usar y cuántas copias (réplicas) quieres, y sabe actualizarlas. Por debajo crea un ReplicaSet, que es el que mantiene el número exacto de pods.
  • Service: un nombre y una IP fijos delante de un grupo de pods. Como los pods cambian, nadie les habla directamente: se le habla al Service y él reparte.
  • ConfigMap y Secret: la configuración y las contraseñas, separadas de la imagen.
  • kubectl: la herramienta de línea de órdenes con la que hablas con el clúster.

Deployment, pods y Service: quién hace qué los objetos de saludo.yaml, tal como quedaron en el clúster de pruebas Deployment «saludo» réplicas: 3 · imagen saludo:1 sabe actualizar y deshacer crea ReplicaSet mantiene 3 pods, ni uno más ni menos ConfigMap «saludo-config» MENSAJE = Hola llega a cada pod como variable pod etiqueta app=saludo saludo:1 :8000 pod etiqueta app=saludo saludo:1 :8000 pod etiqueta app=saludo saludo:1 :8000 Service «saludo» busca: app=saludo dentro: http://saludo IP fija 10.43.13.252 :80 fuera: nodo:30080 reparte entre los 3 pods otra app o el navegador llama al Service Los pods van y vienen y cambian de IP; el nombre y la IP del Service no cambian nunca.

Esquema: decodigo.com. La IP del Service es la que le tocó en la prueba.

Cómo probarlo en tu equipo

No hace falta una nube ni varios servidores. Hay varias formas de tener un Kubernetes de un nodo en tu propio equipo, todas gratuitas:

HerramientaCómo funciona
Docker Desktoptrae un Kubernetes que se activa en los ajustes; lo más fácil si ya lo usas en Windows o macOS
kindcrea clústeres cuyos nodos son contenedores de Docker; muy usado para pruebas
minikubeel clásico para aprender, en una máquina virtual o en un contenedor
k3sun Kubernetes completo y ligero en un solo programa; el que usamos aquí

Nosotros lo arrancamos así, como un contenedor de Docker más (necesita --privileged porque dentro va a crear sus propios contenedores) y le pasamos las dos imágenes de la app sin subirlas a ningún registro:

docker run -d --privileged --hostname nodo-1 --name k8s \
  rancher/k3s:v1.37.1-k3s1 server --disable=traefik --disable=metrics-server

docker save saludo:1 saludo:2 | docker exec -i k8s ctr images import -
docker exec -it k8s sh

Dentro ya tenemos kubectl. Lo primero es ver el clúster:

kubectl get nodes
kubectl version
salidaNAME     STATUS   ROLES           AGE   VERSION
nodo-1   Ready    control-plane   21s   v1.37.1+k3s1

Client Version: v1.37.1+k3s1
Kustomize Version: v5.8.1
Server Version: v1.37.1+k3s1

Paso 1: la imagen, con Docker

La app de prueba es un servidor web de veinte líneas que responde con un mensaje, el nombre del contenedor en el que corre y su versión. Fíjate en las dos líneas de signal: cuando Kubernetes quiere parar un pod, primero le avisa con la señal SIGTERM y espera hasta 30 segundos. Un programa que la ignora tarda eso en morir cada vez que actualizas o escalas.

# Servidor mínimo para las pruebas de la guía de Kubernetes: responde con su versión, el nombre del
# contenedor (en Kubernetes, el del pod) y el mensaje que le llegue por la variable de entorno MENSAJE.
import os, signal, socket, sys
from http.server import BaseHTTPRequestHandler, HTTPServer

VERSION = os.environ.get("VERSION", "?")

class Saludo(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/salud":
            cuerpo = "ok\n"
        else:
            cuerpo = f"{os.environ.get('MENSAJE', 'Hola')} desde {socket.gethostname()} (versión {VERSION})\n"
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()
        self.wfile.write(cuerpo.encode())

    def log_message(self, formato, *args):
        print(f"{self.address_string()} {formato % args}", flush=True)

# Kubernetes avisa con SIGTERM antes de parar un pod; sin esto, Python lo ignoraría y esperaría 30 s al SIGKILL.
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
print(f"saludo {VERSION} escuchando en el puerto 8000", flush=True)
HTTPServer(("0.0.0.0", 8000), Saludo).serve_forever()
FROM python:3.13-slim
ARG VERSION=1
ENV VERSION=${VERSION}
WORKDIR /app
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]

Construimos dos versiones de la misma imagen, que solo cambian en la variable VERSION, para poder actualizar después:

docker build -t saludo:1 --build-arg VERSION=1 .
docker build -t saludo:2 --build-arg VERSION=2 .

Con Docker, arrancar la app y hablarle es esto. Como nombre del equipo, la app ve el identificador del contenedor:

docker run -d --name saludo-docker saludo:1
docker exec saludo-docker python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000').read().decode(), end='')"
salidaHola desde aa4248725aca (versión 1)

Una copia, en un equipo, y si se cae, se queda caída. Vamos a darle lo mismo a Kubernetes.

Paso 2: el manifiesto, lo que quieres que haya

En Kubernetes todo se describe en archivos YAML llamados manifiestos. Pueden ir varios objetos en un archivo, separados por ---. Nuestro saludo.yaml tiene tres. El primero, la configuración:

apiVersion: v1
kind: ConfigMap
metadata:
  name: saludo-config
data:
  MENSAJE: "Hola"

El segundo, el Deployment. Es el más largo, pero se lee de arriba abajo: quiero 3 réplicas; los pods llevan la etiqueta app: saludo; cada uno tiene un contenedor con la imagen saludo:1 que escucha en el 8000 y toma sus variables del ConfigMap.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: saludo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: saludo
  template:
    metadata:
      labels:
        app: saludo
    spec:
      containers:
        - name: saludo
          image: saludo:1
          imagePullPolicy: Never
          ports:
            - containerPort: 8000
          envFrom:
            - configMapRef:
                name: saludo-config
          readinessProbe:
            httpGet:
              path: /salud
              port: 8000
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              memory: 64Mi

Tres detalles que conviene entender:

  • imagePullPolicy: Never le dice que no busque la imagen en internet, porque la importamos a mano. En un clúster de verdad se quita, y la imagen se descarga de un registro.
  • El readinessProbe es una prueba de disponibilidad: Kubernetes llama a /salud y no le manda tráfico al pod hasta que responde. Así nadie llega a una copia que todavía está arrancando.
  • resources reserva un mínimo de CPU y memoria para que el planificador sepa en qué nodo cabe cada pod, y pone un tope de memoria: si lo supera, Kubernetes lo reinicia.

Y el tercero, el Service. Junta todos los pods con la etiqueta app: saludo bajo un solo nombre, saludo, en el puerto 80. Al ser de tipo NodePort, además abre el puerto 30080 en cada nodo para llegar desde fuera del clúster:

apiVersion: v1
kind: Service
metadata:
  name: saludo
spec:
  type: NodePort
  selector:
    app: saludo
  ports:
    - port: 80
      targetPort: 8000
      nodePort: 30080

Paso 3: aplicarlo

kubectl apply le entrega el manifiesto al clúster. Y kubectl rollout status espera a que todo esté listo:

kubectl apply -f saludo.yaml
kubectl rollout status deployment/saludo
salidaconfigmap/saludo-config created
deployment.apps/saludo created
service/saludo created
Waiting for deployment "saludo" rollout to finish: 0 out of 3 new replicas have been updated...
Waiting for deployment "saludo" rollout to finish: 0 of 3 updated replicas are available...
Waiting for deployment "saludo" rollout to finish: 1 of 3 updated replicas are available...
Waiting for deployment "saludo" rollout to finish: 2 of 3 updated replicas are available...
deployment "saludo" successfully rolled out

Ya hay tres pods. Kubernetes les pone nombre a partir del Deployment, y cada uno tiene su IP dentro del clúster:

kubectl get pods -o wide
salidaNAME                      READY   STATUS    RESTARTS   AGE   IP          NODE     NOMINATED NODE   READINESS GATES
saludo-7df8fd8d98-9ntnr   1/1     Running   0          1s    10.42.0.5   nodo-1   <none>           <none>
saludo-7df8fd8d98-ck5qw   1/1     Running   0          1s    10.42.0.6   nodo-1   <none>           <none>
saludo-7df8fd8d98-pql4h   1/1     Running   0          1s    10.42.0.7   nodo-1   <none>           <none>

Y aquí se ve la cadena entera: el Deployment, el ReplicaSet que creó (el nombre de los pods empieza igual) y el Service, con su IP fija. El Service kubernetes viene de serie: es la API del propio clúster.

kubectl get deployment,replicaset,service
salidaNAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/saludo   3/3     3            3           2s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/saludo-7df8fd8d98   3         3         3       2s

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
service/kubernetes   ClusterIP   10.43.0.1      <none>        443/TCP        23s
service/saludo       NodePort    10.43.13.252   <none>        80:30080/TCP   2s

Paso 4: hablar con la app

Dentro del clúster, cualquier pod puede llamar al Service por su nombre, http://saludo: Kubernetes trae su propio DNS. Le hacemos seis peticiones desde uno de los pods:

kubectl exec deploy/saludo -- python -c "
import urllib.request
for _ in range(6): print(urllib.request.urlopen('http://saludo').read().decode(), end='')"
salidaHola desde saludo-7df8fd8d98-9ntnr (versión 1)
Hola desde saludo-7df8fd8d98-9ntnr (versión 1)
Hola desde saludo-7df8fd8d98-9ntnr (versión 1)
Hola desde saludo-7df8fd8d98-ck5qw (versión 1)
Hola desde saludo-7df8fd8d98-pql4h (versión 1)
Hola desde saludo-7df8fd8d98-9ntnr (versión 1)

Contestan los tres pods: el Service reparte las peticiones entre ellos. Para saber a quién reparte, mira sus endpoints, que son justo las IP de los pods que vimos antes:

kubectl get endpointslices -l kubernetes.io/service-name=saludo
salidaNAME           ADDRESSTYPE   PORTS   ENDPOINTS                       AGE
saludo-8nb9d   IPv4          8000    10.42.0.5,10.42.0.7,10.42.0.6   2s

Desde fuera del clúster se entra por el puerto 30080 del nodo. Lo probamos desde otro contenedor:

python -c "import urllib.request; print(urllib.request.urlopen('http://IP-DEL-NODO:30080').read().decode(), end='')"
salida, tres vecesHola desde saludo-7df8fd8d98-6mmkh (versión 1)
Hola desde saludo-7df8fd8d98-xkhjh (versión 1)
Hola desde saludo-7df8fd8d98-6mmkh (versión 1)

Para leer lo que escribe la app, kubectl logs, como docker logs. Si le pasas el Deployment, elige uno de sus pods:

kubectl logs deploy/saludo --tail=3
salidaFound 3 pods, using pod/saludo-7df8fd8d98-9ntnr
10.42.0.1 "GET / HTTP/1.1" 200 -
10.42.0.1 "GET / HTTP/1.1" 200 -
10.42.0.1 "GET / HTTP/1.1" 200 -
En un clúster real, la entrada desde internet no se hace con NodePort, sino con un Service de tipo LoadBalancer, que en las nubes crea un balanceador con IP pública, o con un Ingress o la nueva Gateway API, que reparten por dominio y ruta (/api a un servicio, / a otro) y se encargan del HTTPS.

Paso 5: se repara solo

Aquí está la gran diferencia con Docker. Borramos uno de los pods, como si se hubiera caído:

kubectl delete pod saludo-7df8fd8d98-9ntnr --wait=false
kubectl get pods
salida, un segundo despuéspod "saludo-7df8fd8d98-9ntnr" deleted from default namespace

NAME                      READY   STATUS      RESTARTS   AGE
saludo-7df8fd8d98-9ntnr   0/1     Completed   0          4s
saludo-7df8fd8d98-ck5qw   1/1     Running     0          4s
saludo-7df8fd8d98-ph4sr   0/1     Running     0          1s
saludo-7df8fd8d98-pql4h   1/1     Running     0          4s

El pod borrado está terminando (Completed) y ya hay uno nuevo, ph4sr, con un segundo de vida, arrancando: 0/1 significa que aún no ha pasado la prueba de disponibilidad. Al momento:

salidaNAME                      READY   STATUS    RESTARTS   AGE
saludo-7df8fd8d98-ck5qw   1/1     Running   0          4s
saludo-7df8fd8d98-ph4sr   1/1     Running   0          1s
saludo-7df8fd8d98-pql4h   1/1     Running   0          4s

Otra vez tres. Nadie se lo ha pedido: el ReplicaSet vio que había dos pods cuando el manifiesto dice tres, y creó otro. Es el bucle de control, la idea en la que se basa todo Kubernetes. Si en vez de un pod se cayera un nodo entero, haría lo mismo con todos sus pods, en los nodos que quedan.

El truco de Kubernetes: comparar lo que quieres con lo que hay, sin parar en la prueba borramos un pod y el clúster creó otro en menos de un segundo Estado deseado lo que dice tu YAML replicas: 3 Estado real lo que hay ahora ? 2 pods: uno se ha caído 1. observa el controlador mira el clúster 2. compara quiere 3, hay 2: falta 1 3. actúa crea un pod nuevo y vuelta Con Docker, si borras un contenedor desaparece. En Kubernetes, lo que manda es el YAML.

Esquema: decodigo.com.

Con Docker, en cambio, borrar el contenedor es definitivo:

docker rm -f saludo-docker
docker ps -a --filter name=saludo-docker
salida: el segundo comando ya no muestra nadasaludo-docker

Por eso, en Kubernetes, para quitar algo de verdad no se borra el pod: se cambia el manifiesto o se borra el Deployment.

Paso 6: escalar

¿Llega más tráfico? Se cambia el número de réplicas. Con una orden, o editando replicas: en el YAML y volviendo a aplicarlo:

kubectl scale deployment saludo --replicas=5
kubectl get pods
salidadeployment.apps/saludo scaled

NAME                      READY   STATUS    RESTARTS   AGE
saludo-7df8fd8d98-ck5qw   1/1     Running   0          5s
saludo-7df8fd8d98-gsp7k   1/1     Running   0          1s
saludo-7df8fd8d98-k67tf   1/1     Running   0          1s
saludo-7df8fd8d98-ph4sr   1/1     Running   0          2s
saludo-7df8fd8d98-pql4h   1/1     Running   0          5s

Dos pods nuevos en un segundo, y el Service ya reparte entre los cinco. Kubernetes también puede hacerlo solo según el uso de CPU, con un HorizontalPodAutoscaler, aunque eso necesita el servidor de métricas que en la prueba dejamos apagado. Después volvimos a tres réplicas.

Paso 7: actualizar sin cortes, y deshacer

Para pasar a la versión 2 basta con cambiar la imagen del Deployment:

kubectl set image deployment/saludo saludo=saludo:2
kubectl rollout status deployment/saludo
salidaWaiting for deployment "saludo" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "saludo" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "saludo" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "saludo" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "saludo" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "saludo" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "saludo" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "saludo" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "saludo" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "saludo" rollout to finish: 1 old replicas are pending termination...
deployment "saludo" successfully rolled out

Kubernetes no para los tres pods a la vez: hace una actualización progresiva. Arranca uno nuevo, espera a que pase la prueba de disponibilidad y solo entonces quita uno viejo, y así hasta el final. Durante todo el proceso hay pods respondiendo.

Actualizar sin cortar el servicio: de uno en uno kubectl set image deployment/saludo saludo=saludo:2 al principio saludo:1 saludo:1 saludo:1 arranca uno nuevo saludo:1 saludo:1 saludo:1 saludo:2 arrancando listo: quita uno viejo saludo:1 saludo:1 saludo:2 repite saludo:1 saludo:2 saludo:2 terminado saludo:2 saludo:2 saludo:2 Y si la versión nueva falla En la prueba pedimos saludo:3, una imagen que no existe: el pod nuevo se quedó en ErrImageNeverPull, el reemplazo se detuvo y los tres pods de la versión 2 siguieron respondiendo. kubectl rollout undo volvió a dejar la versión 2.

Esquema: decodigo.com. Por omisión, Kubernetes permite durante el cambio hasta un 25 % de pods de más y un 25 % de menos, redondeando; con tres réplicas, uno de más y ninguno de menos.
las peticiones, ahora a la versión 2Hola desde saludo-85bcd4f69-rp9fr (versión 2)
Hola desde saludo-85bcd4f69-rp9fr (versión 2)
Hola desde saludo-85bcd4f69-rp9fr (versión 2)

Cada cambio queda apuntado como una revisión:

kubectl rollout history deployment/saludo
salidadeployment.apps/saludo 
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

¿Y si la versión nueva viene rota? Lo probamos pidiendo saludo:3, que no existe:

kubectl set image deployment/saludo saludo=saludo:3
kubectl get pods
salida, 20 segundos despuésNAME                      READY   STATUS              RESTARTS   AGE
saludo-5d5ff4b48d-j55v6   0/1     ErrImageNeverPull   0          20s
saludo-85bcd4f69-hq6nz    1/1     Running             0          33s
saludo-85bcd4f69-jcrnv    1/1     Running             0          23s
saludo-85bcd4f69-rp9fr    1/1     Running             0          22s

El pod nuevo no arranca, así que Kubernetes no quita ninguno de los viejos: los tres de la versión 2 siguen atendiendo y la app no se entera.

las peticiones, mientras tantoHola desde saludo-85bcd4f69-rp9fr (versión 2)
Hola desde saludo-85bcd4f69-hq6nz (versión 2)
Hola desde saludo-85bcd4f69-jcrnv (versión 2)

Para saber qué le pasa a un pod, kubectl describe pod y mirar los eventos del final. Es la primera orden que hay que usar cuando algo no arranca:

salida de kubectl describe pod saludo-5d5ff4b48d-j55v6, solo los eventosEvents:
  Type     Reason             Age               From               Message
  ----     ------             ----              ----               -------
  Normal   Scheduled          20s               default-scheduler  Successfully assigned default/saludo-5d5ff4b48d-j55v6 to nodo-1
  Warning  ErrImageNeverPull  8s (x3 over 20s)  kubelet            spec.containers{saludo}: Container image "saludo:3" is not present with pull policy of Never
  Warning  Failed             8s (x3 over 20s)  kubelet            spec.containers{saludo}: Error: ErrImageNeverPull

Y para volver atrás, kubectl rollout undo recupera la revisión anterior, la versión 2:

kubectl rollout undo deployment/saludo
salida, y las peticiones despuésWarning: resource deployments/saludo was previously managed with 'kubectl apply'. Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation, which may cause unexpected behavior on future 'kubectl apply' operations. Consider using 'kubectl apply' with your previous configuration file instead.
deployment.apps/saludo rolled back

Hola desde saludo-85bcd4f69-hq6nz (versión 2)
Hola desde saludo-85bcd4f69-hq6nz (versión 2)
Hola desde saludo-85bcd4f69-rp9fr (versión 2)

El aviso es importante: rollout undo cambia el clúster, pero no tu archivo, que todavía dice saludo:1. En el día a día, la buena práctica es la contraria: cambiar siempre el YAML, guardarlo en Git y aplicarlo con kubectl apply. Así el archivo y el clúster nunca se contradicen.

Paso 8: cambiar la configuración

La configuración vive en el ConfigMap, no en la imagen. Cambiamos el mensaje sin reconstruir nada:

kubectl create configmap saludo-config --from-literal=MENSAJE="Buenas tardes" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment/saludo
salida, y las peticiones despuésconfigmap/saludo-config configured
deployment.apps/saludo restarted

Buenas tardes desde saludo-69988bf4cb-4wdsq (versión 2)
Buenas tardes desde saludo-69988bf4cb-c95bl (versión 2)
Buenas tardes desde saludo-69988bf4cb-c95bl (versión 2)

Las variables de entorno se leen al arrancar el contenedor, así que hay que reiniciar los pods; rollout restart lo hace de forma progresiva, igual que una actualización. Las contraseñas no van en un ConfigMap, sino en un Secret, que se usa igual pero se guarda aparte y se puede proteger mejor.

Para terminar, todo lo que creó el manifiesto se borra con el mismo archivo:

kubectl delete -f saludo.yaml
salidaconfigmap "saludo-config" deleted from default namespace
deployment.apps "saludo" deleted from default namespace
service "saludo" deleted from default namespace

Los errores que más vas a ver

Aparecen en la columna STATUS de kubectl get pods. Para cualquiera de ellos, el siguiente paso es kubectl describe pod (los eventos) y kubectl logs (lo que escribió la app).

EstadoQué significaQué mirar
ImagePullBackOff · ErrImagePullno puede descargar la imagenel nombre y la etiqueta de la imagen, y si el registro es privado, sus credenciales
ErrImageNeverPullla imagen no está en el nodo y le prohibiste descargarlala que vimos en la prueba: importar la imagen o quitar imagePullPolicy: Never
CrashLoopBackOffel contenedor arranca y se cae una y otra vezkubectl logs --previous: casi siempre es un error de la app o una variable que falta
Pendingno hay ningún nodo donde quepalos eventos: suele faltar CPU o memoria para lo que pide resources
OOMKilledsuperó su límite de memoria y lo mataronsubir limits.memory o buscar la fuga de memoria
Running pero 0/1arrancó, pero no pasa la prueba de disponibilidadque la ruta y el puerto del readinessProbe sean los de la app

Resumen

Quiero…Orden
aplicar o actualizar lo que dice un archivokubectl apply -f saludo.yaml
ver los pods, con su IP y su nodokubectl get pods -o wide
ver todo lo de un vistazokubectl get deployment,service,pods
saber por qué falla un podkubectl describe pod NOMBRE
leer lo que escribe la appkubectl logs deploy/saludo
entrar en un pod o ejecutar algo en élkubectl exec -it deploy/saludo -- sh
cambiar el número de copiaskubectl scale deployment saludo --replicas=5
pasar a otra versiónkubectl set image deployment/saludo saludo=saludo:2
seguir una actualizaciónkubectl rollout status deployment/saludo
volver a la versión anteriorkubectl rollout undo deployment/saludo
reiniciar los pods sin corteskubectl rollout restart deployment/saludo
llegar a un pod desde tu equipo para probarkubectl port-forward svc/saludo 8080:80
borrar lo que creó un archivokubectl delete -f saludo.yaml

Esta guía es la continuación natural de la guía de Docker: allí aprendes a construir la imagen, aquí a ponerla a funcionar a escala. Si quieres entender qué pasa por debajo cuando un Service reparte tráfico o abre un puerto, tienes también la guía de redes básicas y la de DNS.

Las pruebas se ejecutaron el 15 de octubre de 2026 con Docker 29.8.1 y k3s v1.37.1 (Kubernetes 1.37.1) en un contenedor desechable, borrado al terminar junto con sus imágenes; las salidas están copiadas de esa ejecución, y los nombres de los pods y las IP cambian en cada una. La orden kubectl port-forward del resumen no se ejecutó. Conceptos, objetos y valores por omisión según la documentación oficial de Kubernetes; versiones vigentes en kubernetes.io/releases; el cambio de motor de contenedores, en las preguntas frecuentes sobre la retirada de dockershim.

Fuente: Kubernetes, «Overview»