Un contenedor es una unidad de software que empaqueta una aplicación junto con todas sus dependencias (librerías, runtime, ficheros de configuración) de forma que se ejecuta igual en cualquier entorno: el portátil del desarrollador, un servidor de pruebas o la nube en producción. Resuelve el clásico "en mi máquina funciona". Docker es la herramienta más popular para construir y ejecutar contenedores, y Kubernetes es el orquestador estándar de la industria para gestionar miles de contenedores en producción: los despliega, los escala, los reinicia si fallan y los conecta entre sí. Dominar ambos es imprescindible en arquitectura cloud-native, porque casi toda aplicación moderna se entrega en contenedores. Esta lección te lleva desde el concepto hasta un despliegue real en Kubernetes.

Contenido

  1. Contenedores frente a máquinas virtuales.
  2. Anatomía de Docker: imágenes, contenedores y registros.
  3. Construir una imagen con un Dockerfile.
  4. Por qué necesitamos un orquestador.
  5. Conceptos clave de Kubernetes: Pod, Deployment y Service.
  6. Un manifiesto de Kubernetes paso a paso.
  7. Errores comunes y consejos.
  8. Ejercicios y soluciones.

  1. Contenedores frente a máquinas virtuales

Ambos aíslan aplicaciones, pero de forma muy distinta. Una máquina virtual (VM) virtualiza el hardware completo e incluye un sistema operativo invitado entero. Un contenedor virtualiza solo el sistema operativo: comparte el kernel del anfitrión y empaqueta únicamente la aplicación y sus dependencias.

Aspecto Máquina Virtual Contenedor
Aislamiento Hardware completo (hipervisor) Proceso (namespaces del kernel)
Sistema operativo Uno completo por VM Comparte el kernel del anfitrión
Tamaño Gigabytes Megabytes
Tiempo de arranque Minutos Segundos o menos
Densidad (cuántos por host) Pocos Muchos
Portabilidad Media Muy alta
graph TB
    subgraph "Maquinas Virtuales"
        HW1[Hardware] --> HYP[Hipervisor]
        HYP --> VM1[SO Invitado + App A]
        HYP --> VM2[SO Invitado + App B]
    end
    subgraph "Contenedores"
        HW2[Hardware] --> OS[SO Anfitrion]
        OS --> DOCK[Docker Engine]
        DOCK --> C1[App A]
        DOCK --> C2[App B]
    end

Explicación del diagrama: en las VMs cada aplicación arrastra su propio sistema operativo completo sobre un hipervisor, lo que consume muchos recursos. En los contenedores, el motor (Docker) comparte un único sistema operativo anfitrión, así que los contenedores son ligeros y arrancan en segundos. No son enemigos: a menudo se ejecutan contenedores dentro de VMs.

  1. Anatomía de Docker

Tres conceptos que no debes confundir:

  • Imagen (image): plantilla inmutable de solo lectura que contiene tu aplicación y dependencias. Es como una "foto" o molde.
  • Contenedor (container): una instancia en ejecución de una imagen. De una imagen puedes arrancar muchos contenedores.
  • Registro (registry): almacén de imágenes (Docker Hub, GitHub Container Registry, AWS ECR). Subes (push) y descargas (pull) imágenes desde ahí.
# Construir una imagen a partir del Dockerfile del directorio actual
docker build -t miapp:1.0 .

# Ejecutar un contenedor a partir de la imagen, publicando el puerto 8080
docker run -d -p 8080:8080 --name miapp miapp:1.0

# Ver los contenedores en ejecucion
docker ps

# Subir la imagen a un registro
docker push miregistro.io/miapp:1.0

Explicación de los comandos:

  • docker build -t miapp:1.0 .: construye la imagen. -t la etiqueta con nombre miapp y versión 1.0; el . indica que el Dockerfile está en el directorio actual.
  • docker run -d -p 8080:8080: arranca un contenedor. -d lo ejecuta en segundo plano (detached); -p 8080:8080 mapea el puerto del anfitrión al del contenedor para poder acceder desde fuera.
  • docker ps: lista los contenedores activos, su estado y puertos.
  • docker push: publica la imagen en un registro para que otros (o un clúster) la descarguen.

  1. Construir una imagen con un Dockerfile

Un Dockerfile es un fichero de texto con instrucciones para construir la imagen, paso a paso. Cada instrucción crea una "capa" cacheable. Veamos un ejemplo realista con multi-stage build (compilación en varias etapas) para una aplicación Java:

# --- Etapa 1: compilacion ---
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

# --- Etapa 2: imagen final, ligera ---
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=build /app/target/miapp.jar app.jar
EXPOSE 8080
USER 1000
ENTRYPOINT ["java", "-jar", "app.jar"]

Explicación instrucción a instrucción:

  • FROM maven:... AS build: parte de una imagen base con Maven y Java para compilar. La nombramos build para referirnos a ella después.
  • WORKDIR /app: establece el directorio de trabajo dentro de la imagen.
  • COPY pom.xml . + RUN mvn dependency:go-offline: copiamos primero solo el pom.xml y descargamos dependencias. Así, si el código cambia pero las dependencias no, Docker reutiliza esta capa de la caché y la construcción es más rápida.
  • COPY src ./src + RUN mvn package: copia el código fuente y genera el .jar.
  • Segundo FROM eclipse-temurin:17-jre-alpine: empieza una imagen final mucho más pequeña (solo el runtime de Java, sobre Alpine Linux). Esta técnica deja fuera Maven y el código fuente, reduciendo el tamaño y la superficie de ataque.
  • COPY --from=build ...: copia solo el artefacto compilado desde la etapa anterior.
  • EXPOSE 8080: documenta el puerto que usa la app.
  • USER 1000: ejecuta el proceso con un usuario sin privilegios (no root), buena práctica de seguridad.
  • ENTRYPOINT [...]: comando que se ejecuta al arrancar el contenedor.

  1. Por qué necesitamos un orquestador

Ejecutar uno o dos contenedores con docker run es fácil. ¿Pero qué pasa con cientos de contenedores en decenas de servidores? Necesitas resolver: ¿en qué servidor coloco cada contenedor? ¿qué hago si un contenedor se cae? ¿cómo escalo de 3 a 30 réplicas en un pico de tráfico? ¿cómo actualizo sin tiempo de inactividad? ¿cómo se encuentran entre sí? Hacerlo a mano es inviable. Un orquestador como Kubernetes automatiza todo esto.

  1. Conceptos clave de Kubernetes

Kubernetes (abreviado K8s) organiza los contenedores con varias abstracciones. Las tres fundamentales:

Objeto Qué es Analogía
Pod Unidad mínima desplegable: uno o varios contenedores que comparten red y almacenamiento Una "cápsula" con tu app
Deployment Gestiona réplicas de Pods y actualizaciones controladas El "gestor" que mantiene N copias vivas
Service Punto de acceso de red estable hacia un conjunto de Pods La "recepción" con dirección fija
  • Pod: lo más pequeño que despliega Kubernetes. Normalmente un contenedor por Pod. Los Pods son efímeros: pueden morir y recrearse con otra IP.
  • Deployment: declaras "quiero 3 réplicas de esta imagen" y Kubernetes las mantiene. Si un Pod muere, crea otro. Permite rolling updates (actualizaciones sin caída).
  • Service: como los Pods cambian de IP, el Service da una dirección estable y reparte el tráfico entre los Pods sanos (balanceo de carga interno).
graph LR
    USER[Usuario] --> SVC[Service: IP estable]
    SVC --> P1[Pod 1]
    SVC --> P2[Pod 2]
    SVC --> P3[Pod 3]
    DEP[Deployment] -.gestiona.-> P1
    DEP -.gestiona.-> P2
    DEP -.gestiona.-> P3

Explicación: el Deployment crea y vigila los tres Pods. El Service recibe el tráfico del usuario y lo reparte entre los Pods disponibles. Si el Pod 2 cae, el Deployment crea uno nuevo y el Service deja de enviarle tráfico hasta que esté listo.

  1. Un manifiesto de Kubernetes paso a paso

En Kubernetes describes el estado deseado en ficheros YAML declarativos. Aquí un Deployment y un Service para nuestra aplicación:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: miapp-deployment
spec:
  replicas: 3                  # numero de Pods deseados
  selector:
    matchLabels:
      app: miapp               # selecciona los Pods con esta etiqueta
  template:                    # plantilla de cada Pod
    metadata:
      labels:
        app: miapp
    spec:
      containers:
        - name: miapp
          image: miregistro.io/miapp:1.0
          ports:
            - containerPort: 8080
          resources:           # limites de recursos
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          readinessProbe:      # cuando esta listo para recibir trafico
            httpGet:
              path: /health
              port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: miapp-service
spec:
  selector:
    app: miapp                 # enruta hacia los Pods con esta etiqueta
  ports:
    - port: 80                 # puerto del Service
      targetPort: 8080         # puerto del contenedor
  type: ClusterIP              # accesible solo dentro del cluster

Explicación campo a campo:

  • kind: Deployment y replicas: 3: pedimos un Deployment que mantenga tres réplicas.
  • selector.matchLabels + template.metadata.labels: el Deployment vincula los Pods que tengan la etiqueta app: miapp. Las etiquetas son el pegamento de Kubernetes.
  • image: la imagen del registro que vimos antes.
  • resources.requests/limits: requests es lo que el Pod reserva como mínimo; limits es el máximo que puede consumir. 250m significa 0,25 de CPU; 256Mi son 256 mebibytes de RAM. Esto evita que un Pod ahogue a los demás.
  • readinessProbe: Kubernetes consulta /health para saber cuándo el Pod está listo. Hasta que responda OK, el Service no le envía tráfico.
  • En el Service, selector: app: miapp conecta el Service con esos Pods; port: 80 es donde escucha el Service y targetPort: 8080 es el puerto del contenedor; type: ClusterIP lo hace accesible solo internamente (para exponerlo al exterior se usa LoadBalancer o un Ingress).
  • El --- separa dos documentos YAML en el mismo fichero.
# Aplicar el manifiesto al cluster
kubectl apply -f miapp.yaml

# Ver el estado de los Pods
kubectl get pods

# Escalar a 5 replicas en caliente
kubectl scale deployment miapp-deployment --replicas=5

Explicación: kubectl apply envía el estado deseado al clúster, que se encarga de alcanzarlo. kubectl get pods muestra los Pods y su estado. kubectl scale cambia el número de réplicas sin editar el fichero.

Errores Comunes y Consejos

  • Construir imágenes enormes. Usa multi-stage builds e imágenes base ligeras (alpine, distroless). Una imagen de 1 GB es lenta de desplegar y un riesgo de seguridad.
  • Ejecutar como root. Define un USER no privilegiado en el Dockerfile. Un contenedor comprometido como root es más peligroso.
  • No definir requests y limits. Sin límites, un Pod puede consumir toda la memoria del nodo y tumbar a sus vecinos.
  • Olvidar las probes. Sin readinessProbe/livenessProbe, Kubernetes envía tráfico a Pods que aún no están listos o no reinicia los que se han colgado.
  • Tratar los Pods como mascotas. Los Pods son ganado, no mascotas: son efímeros y reemplazables. No guardes estado dentro de ellos; usa volúmenes o servicios externos.
  • Consejo: versiona siempre las imágenes con etiquetas explícitas (miapp:1.0), nunca confíes en latest en producción porque es ambiguo y dificulta los rollbacks.

Ejercicios

  1. Explica con tus palabras por qué un contenedor arranca en segundos y una VM en minutos.
  2. En el manifiesto de ejemplo, ¿qué ocurre si eliminas manualmente uno de los tres Pods del Deployment? ¿Por qué?
  3. Quieres exponer tu aplicación al tráfico externo de Internet. El Service del ejemplo es de tipo ClusterIP. ¿Qué cambiarías y qué alternativas tienes?

Soluciones

  1. El contenedor comparte el kernel del sistema operativo anfitrión y solo arranca el proceso de la aplicación con sus dependencias, mientras que la VM debe arrancar un sistema operativo invitado completo (kernel, servicios, etc.) sobre el hipervisor. Menos que arrancar implica menos tiempo.
  2. El Deployment detecta que solo hay 2 Pods cuando el estado deseado son 3 y crea automáticamente un Pod nuevo para volver a 3 réplicas. Kubernetes reconcilia continuamente el estado real con el declarado.
  3. Cambiarías type: ClusterIP por type: LoadBalancer (que aprovisiona un balanceador de carga del proveedor cloud con IP pública) o, de forma más habitual y flexible, mantendrías el Service como ClusterIP y colocarías delante un Ingress con reglas de enrutamiento por host/ruta y terminación TLS.

Conclusión

Has aprendido la diferencia entre contenedores y VMs, cómo Docker empaqueta aplicaciones mediante imágenes y Dockerfiles, por qué hace falta un orquestador y los tres pilares de Kubernetes (Pod, Deployment, Service) plasmados en un manifiesto YAML real. Los contenedores te dan portabilidad y Kubernetes te da escala y resiliencia. En la siguiente lección exploraremos un modelo aún más abstracto en el que ni siquiera gestionas contenedores ni servidores: la arquitectura serverless.

Curso de Arquitectura de Aplicaciones

Módulo 1: Fundamentos de la Arquitectura de Aplicaciones

Módulo 2: Principios y Tácticas de Diseño

Módulo 3: Estilos y Patrones Arquitectónicos

Módulo 4: Arquitecturas Distribuidas y Microservicios

Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería

Módulo 6: Diseño Dirigido por el Dominio (DDD)

Módulo 7: Datos y Persistencia

Módulo 8: Arquitectura en la Nube y Despliegue

Módulo 9: Calidad, Seguridad y Observabilidad

Módulo 10: Evolución, Gobernanza y Casos Prácticos

© Copyright 2026. Todos los derechos reservados