Aurora Libros ya se construye y se despliega sola, pero sigue viviendo en una máquina. Si esa máquina se apaga, la librería desaparece de internet. Esta lección monta tu primer clúster con Docker Swarm: la orquestación más cercana a lo que ya sabes, porque habla compose.yaml y comandos docker.
Contenido
- Qué resuelve un orquestador
- Arquitectura de Swarm: managers, workers y Raft
- El estado deseado y el bucle de reconciliación
- Montar el clúster:
init,joiny puertos - Gestionar nodos: promover, degradar y drenar
- Servicios y tareas
- Réplicas frente a modo global
- Escalado y restricciones de ubicación
- Redes overlay: VXLAN entre nodos
- La malla de enrutamiento
- Stacks: el
compose.yamlque ya tienes - La sección
deploycompleta - Secretos y configs nativos de Swarm
- Aurora Libros en un clúster de tres nodos
- Swarm en 2026: cuándo sigue siendo la elección
Advertencia. Un clúster multiplica las decisiones de red, almacenamiento y control de acceso: los puertos entre nodos, el cifrado del tráfico interno y la ubicación de los datos persistentes deben acordarse con el responsable de infraestructura y de seguridad de tu organización antes de llevar nada a producción.
- Qué resuelve un orquestador
| Problema | Compose en una máquina | Orquestador |
|---|---|---|
| La máquina se apaga | Todo cae | Las tareas se reprograman en otros nodos |
| Un contenedor muere | restart: lo reinicia ahí mismo |
Se recrea donde haya sitio |
| Hace falta más capacidad | Solo si cabe en esa máquina | Se añade un nodo al clúster |
| Publicar una versión | Recrear: hueco sin servicio | Sustitución progresiva, sin corte |
| ¿Dónde colocar cada servicio? | No hay elección | Planificador según recursos y reglas |
| Balancear entre réplicas | Nginx a mano | Balanceo integrado por nombre de servicio |
| Un despliegue sale mal | Vuelves a desplegar a mano | Rollback automático (06-07) |
Un orquestador aporta tres cosas que Compose en una sola máquina no puede dar por definición: planificación (decidir en qué nodo va cada cosa), reconciliación (mantener el estado deseado pase lo que pase) y red entre máquinas (que los contenedores se hablen aunque estén en anfitriones distintos).
- Arquitectura de Swarm: managers, workers y Raft
flowchart TB
subgraph P["Plano de control — Raft"]
M1["manager-1 (líder)"] <--> M2[manager-2]
M2 <--> M3[manager-3]
M3 <--> M1
end
M1 -->|asigna tareas| W1[worker-1]
M1 -->|asigna tareas| W2[worker-2]
W1 --- W2
style M1 fill:#e8f0fe,stroke:#3367d6
| Rol | Qué hace | Cuántos |
|---|---|---|
| Manager | Guarda el estado, planifica, expone la API, participa en Raft | 1, 3, 5 o 7 (impar) |
| Líder | El manager elegido que toma las decisiones de planificación | 1, elegido automáticamente |
| Worker | Solo ejecuta tareas; no toma decisiones | Los que hagas falta |
El estado del clúster —qué servicios existen, cuántas réplicas, qué secretos— se replica entre los managers con el algoritmo de consenso Raft. Para aceptar un cambio hace falta el acuerdo de la mayoría, el quórum, que es (N/2) + 1.
De ahí sale la regla del número impar, que no es superstición sino aritmética:
| Managers | Quórum | Fallos tolerados | Comentario |
|---|---|---|---|
| 1 | 1 | 0 | Desarrollo. Si cae, no hay clúster |
| 2 | 2 | 0 | Peor que uno: cualquier caída bloquea |
| 3 | 2 | 1 | El mínimo razonable en producción |
| 4 | 3 | 1 | Igual que 3, con más coste |
| 5 | 3 | 2 | Clústeres grandes |
La fila de los dos managers es la que sorprende: añadir un segundo manager empeora la disponibilidad, porque necesitas los dos vivos para alcanzar el quórum. Y hay una consecuencia importante que conviene tener clara: perder el quórum no para los contenedores en marcha, pero deja el clúster sin capacidad de decidir; no se puede escalar, ni desplegar, ni reprogramar nada hasta que vuelvan suficientes managers.
- El estado deseado y el bucle de reconciliación
Aquí está el cambio mental respecto a todo lo anterior. Con docker run das órdenes; con un orquestador declaras cómo quieres que esté el mundo y él se encarga.
El líder ejecuta un bucle permanente: compara el estado deseado (lo que has declarado) con el estado real (lo que hay), y si difieren crea o elimina tareas hasta que coincidan. Cuando coinciden, espera al siguiente cambio y vuelve a comparar.
docker service create --replicas 3 aurora-api no significa "arranca tres contenedores": significa "quiero que siempre haya tres". Mata uno y aparece otro. Apaga un nodo entero y sus tareas renacen en los que quedan. Nadie ha ejecutado un comando de reparación: el bucle simplemente ha notado una diferencia entre lo deseado y lo real y la ha corregido.
- Montar el clúster:
init, join y puertos
init, join y puertosdocker swarm init --advertise-addr 10.0.1.10 # en manager-1
# Swarm initialized: current node (k3f9x...) is now a manager.
# To add a worker to this swarm, run the following command:
# docker swarm join --token SWMTKN-1-49nj1...-8vxv8 10.0.1.10:2377El --advertise-addr es obligatorio en cuanto la máquina tenga más de una interfaz, y omitirlo es el primer error clásico: Swarm elige una IP cualquiera —a menudo la pública, o una de una VPN— y los demás nodos no consiguen alcanzarla.
docker swarm join-token worker # los tokens son distintos por rol
docker swarm join-token --rotate manager # si un token se ha filtrado
docker swarm join --token SWMTKN-1-49nj1...-8vxv8 10.0.1.10:2377 # en cada worker
docker node ls # desde cualquier manager
# ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE
# k3f9x * manager-1 Ready Active Leader 27.3.1
# p8m2q worker-1 Ready Active 27.3.1
# r5t7w worker-2 Ready Active 27.3.1| Puerto | Protocolo | Para qué | Entre |
|---|---|---|---|
| 2377 | TCP | API de gestión del clúster | Nodos → managers |
| 7946 | TCP y UDP | Descubrimiento y gossip entre nodos | Todos ↔ todos |
| 4789 | UDP | Tráfico de datos VXLAN de las redes overlay | Todos ↔ todos |
Estos tres puertos causan la mitad de los problemas de un clúster nuevo, y siempre por lo mismo: 7946 necesita TCP y UDP, y 4789 es UDP. Un cortafuegos que solo abra TCP produce el síntoma más desconcertante posible: los nodos aparecen Ready, los servicios se despliegan, pero los contenedores de nodos distintos no se ven entre sí. Nunca abras el 2377 a internet: quien lo alcance con un token puede unirse a tu clúster.
- Gestionar nodos: promover, degradar y drenar
docker node promote worker-1 worker-2 # de worker a manager
docker node demote manager-3 # de manager a worker
docker node inspect worker-1 --format '{{.Status.State}} {{.Spec.Availability}}'
docker node update --label-add zona=a --label-add disco=ssd worker-1Availability |
Tareas nuevas | Tareas existentes | Cuándo |
|---|---|---|---|
active |
Sí | Siguen | Normal |
pause |
No | Siguen | Investigar sin que lleguen más |
drain |
No | Se mueven a otros nodos | Mantenimiento o retirada |
docker node update --availability drain worker-1 # vaciar antes de tocar la máquina
# ... mantenimiento, reinicio, actualización del kernel ...
docker node update --availability active worker-1El ciclo drain → mantenimiento → active es la rutina básica de operación de un clúster: las tareas se recolocan solas antes de que toques nada, y nadie se queda sin servicio. Ojo con un detalle: al volver a active, las tareas no regresan solas al nodo; se quedan donde están hasta el siguiente despliegue.
- Servicios y tareas
Swarm introduce dos conceptos nuevos sobre lo que ya conoces:
- Servicio: la declaración. "Quiero tres réplicas de esta imagen con esta configuración".
- Tarea: cada unidad de trabajo asignada a un nodo. Una tarea acaba siendo un contenedor, y es inmutable: no se modifica ni se reinicia, se sustituye por otra nueva.
docker service create --name aurora-api --replicas 3 \
--network aurora-trasera --env DB_HOST=aurora-db --publish published=8080,target=3000 \
--limit-memory 512M --limit-cpu 1.0 --reserve-memory 128M \
--health-interval 10s --health-retries 3 ghcr.io/auroralibros/aurora-api:2.0.0| Comando | Qué muestra | Equivalente que ya conoces |
|---|---|---|
docker service ls |
Servicios y réplicas listas | docker compose ps |
docker service ps <svc> |
Cada tarea, su nodo y su historial | docker ps por servicio |
docker service logs -f <svc> |
Logs agregados de todas las réplicas | docker compose logs -f |
docker service inspect <svc> |
Especificación completa | docker inspect |
docker service update <svc> |
Cambia el estado deseado | Editar y up -d |
docker service rm <svc> |
Elimina el servicio y sus tareas | docker compose rm |
docker service ps es la herramienta de diagnóstico principal, porque muestra también las tareas muertas y por qué murieron:
NAME NODE DESIRED STATE CURRENT STATE ERROR
aurora-api.1 worker-1 Running Running 4 minutes ago
aurora-api.2 worker-2 Running Running 4 minutes ago
aurora-api.3 worker-2 Running Running 40 seconds ago
\_ aurora-api.3 worker-1 Shutdown Failed 45 seconds ago "task: non-zero exit (78)"Esa última línea cuenta una historia completa: la tarea 3 murió en worker-1 con el código 78 que programaste en 06-01, es decir, configuración inválida, y el orquestador la recreó en worker-2. Sin la validación de arranque, ahí pondría un genérico exit (1).
- Réplicas frente a modo global
| Modo | Cuántas tareas | Al añadir un nodo | Para qué |
|---|---|---|---|
--mode replicated (defecto) |
Las que pidas | No cambia | Aplicaciones: aurora-api, aurora-web |
--mode global |
Una por nodo, siempre | Aparece una nueva | Agentes: Promtail, cAdvisor, node-exporter |
--mode replicated-job |
N ejecuciones y termina | — | Migraciones, tareas puntuales |
El modo global es exactamente lo que necesitan los recolectores de métricas y logs del módulo 5: cada nodo necesita su cAdvisor y su Promtail, y quieres que aparezcan solos en cada máquina que añadas al clúster sin acordarte de nada.
- Escalado y restricciones de ubicación
docker service scale aurora-api=5 aurora-web=3 # varios a la vez
docker service update --replicas 2 aurora-api # equivalente
# Restricciones DURAS: si no se cumplen, la tarea no se programa (queda Pending)
docker service update --constraint-add 'node.labels.disco==ssd' aurora-db
# Preferencias BLANDAS: reparten, pero no impiden
docker service update --placement-pref 'spread=node.labels.zona' aurora-api| Expresión | Significado |
|---|---|
node.role==worker |
Solo en workers (mantiene los managers descargados) |
node.labels.zona==a |
Solo en nodos con esa etiqueta |
node.hostname!=worker-2 |
En cualquiera menos ese |
spread=node.labels.zona |
Reparto equitativo entre zonas |
La diferencia entre restricción y preferencia importa mucho en la práctica: una restricción imposible deja el servicio en Pending para siempre, sin ningún error evidente, mientras que una preferencia solo influye en el reparto. Si un servicio no arranca y docker service ps no muestra ningún error, sospecha de una restricción que ningún nodo cumple.
- Redes overlay: VXLAN entre nodos
La red bridge de 03-05 solo conecta contenedores del mismo anfitrión. Una red overlay conecta contenedores de nodos distintos como si compartieran un cable.
docker network create -d overlay --attachable --subnet 10.10.0.0/24 aurora-frontal
docker network create -d overlay --opt encrypted --internal aurora-traseraPor dentro es un túnel VXLAN: cada paquete Ethernet del contenedor se encapsula dentro de un datagrama UDP hacia el puerto 4789 del nodo destino, que lo desencapsula y lo entrega. Para los contenedores, la existencia de dos máquinas es invisible; siguen resolviendo aurora-db por DNS y hablando por su IP de la red overlay.
| Opción | Efecto | Coste |
|---|---|---|
--attachable |
Permite conectar contenedores sueltos, no solo servicios | Ninguno |
--opt encrypted |
Cifra IPsec el tráfico entre nodos | 10-30 % de rendimiento |
--internal |
Sin salida a internet | Ninguno |
--subnet |
Rango fijo, evita colisiones con tu red corporativa | Ninguno |
El --opt encrypted merece una decisión consciente. El tráfico VXLAN viaja en claro por defecto: si tus nodos están en una red privada de un mismo centro de datos puede ser aceptable, pero si cruzan internet o una red compartida, el catálogo, las consultas y las credenciales que pasen por ahí son legibles para cualquiera con acceso al medio. Es exactamente el tipo de decisión que debes validar con tu responsable de seguridad. Ojo también: el cifrado se aplica al plano de datos, y no puede activarse ni desactivarse sin recrear la red.
- La malla de enrutamiento
flowchart TB
C[Cliente] -->|:8080| N2["worker-2<br/>(sin réplica local)"]
N2 -->|IPVS por la overlay| T1["tarea 1 @ worker-1"]
N2 --> T2["tarea 2 @ manager-1"]
style N2 fill:#fff4e5,stroke:#e8a33d
Cuando publicas un puerto en modo ingress (el predeterminado), todos los nodos del clúster escuchan en él, incluso los que no ejecutan ninguna réplica de ese servicio. El nodo que recibe la conexión la reparte por IPVS entre las tareas vivas, estén donde estén.
La ventaja es enorme para el balanceador de entrada: apunta a cualquier nodo, o a todos, y no necesita saber dónde está nada. Los inconvenientes son dos y conviene conocerlos: hay un salto de red extra cuando el nodo que recibe no tiene la tarea, y la IP de origen que ve tu aplicación es la del nodo, no la del cliente real, porque hay SNAT de por medio.
| Modo de publicación | Sintaxis | Comportamiento |
|---|---|---|
ingress (defecto) |
--publish 8080:3000 |
Todos los nodos escuchan; balanceo por IPVS |
host |
--publish mode=host,published=8080,target=3000 |
Solo los nodos con réplica; IP real del cliente, sin salto extra |
El modo host es la salida cuando necesitas la IP del cliente (registro de acceso, límites por IP, geolocalización) o el último gramo de latencia, y tiene un precio: obliga al balanceador externo a saber en qué nodos hay réplicas, y no puedes tener dos réplicas del mismo servicio en un nodo, porque chocarían en el puerto.
- Stacks: el
compose.yaml que ya tienes
compose.yaml que ya tienesAquí llega la recompensa de todo el módulo 4: docker stack deploy acepta tu compose.yaml.
docker stack deploy -c compose.swarm.yaml --with-registry-auth aurora
docker stack ls
docker stack services aurora
docker stack ps aurora --no-trunc
docker stack rm auroraEl --with-registry-auth es imprescindible con un registro privado como ghcr.io: sin él, el manager entiende las credenciales pero no las reenvía a los workers, y las tareas fallan con no basic auth credentials en todos los nodos menos donde hiciste docker login.
| Clave de Compose | En Swarm |
|---|---|
image, environment, networks, ports, secrets, configs, healthcheck |
Funcionan igual |
deploy: |
Solo aquí tiene efecto (Compose la ignoraba salvo resources) |
build: |
Ignorada: hay que publicar la imagen en un registro |
depends_on: |
Ignorada: no hay orden de arranque, hay reintentos |
restart:, container_name, develop, profiles |
Ignoradas |
volumes: con ruta relativa |
Peligrosa: se resuelve en cada nodo |
Las dos ignoradas de en medio cambian cómo diseñas la aplicación. Sin build, el flujo obliga a pasar por el registro, lo cual es correcto en producción. Y sin depends_on, tu API tiene que soportar arrancar antes que la base de datos: por eso implementaste el reintento con backoff en 04-04, y por eso las sondas de 06-01 devuelven 503 mientras las dependencias no estén.
- La sección
deploy completa
deploy completaservices:
aurora-api:
image: ghcr.io/auroralibros/aurora-api:2.0.0
deploy:
mode: replicated
replicas: 3
placement:
constraints: ["node.role==worker"]
preferences: [{ spread: node.labels.zona }]
max_replicas_per_node: 2
resources:
limits: { cpus: "1.0", memory: 512M }
reservations: { cpus: "0.1", memory: 128M } # el planificador SÍ las respeta
restart_policy:
condition: on-failure # any | on-failure | none
delay: 5s
max_attempts: 3
window: 120s
update_config: # se detalla en 06-07
parallelism: 1
delay: 10s
order: start-first
failure_action: rollback
rollback_config: { parallelism: 2, order: stop-first }
labels: { com.aurora.componente: api }Un matiz que separa reservations de limits y que en Compose no existía: en Swarm, las reservas afectan a la planificación. El planificador suma las reservas de las tareas ya ubicadas en cada nodo y solo coloca una nueva donde quepa. Reservar de más deja nodos vacíos con servicios en Pending; reservar de menos amontona tareas hasta que el nodo se queda sin memoria. Los números medidos de 06-01 son justo lo que necesitas aquí.
- Secretos y configs nativos de Swarm
printf 'clave-ficticia-aurora' | docker secret create aurora_db_password -
docker config create aurora_init_sql ./db/init.sql
docker secret ls && docker config lsservices:
aurora-api:
secrets:
- source: aurora_db_password
target: db_password # queda en /run/secrets/db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password # el patrón _FILE de 06-01, intacto
secrets:
aurora_db_password:
external: true
configs:
aurora_init_sql:
external: trueLos secretos viajan a los nodos cifrados por TLS mutuo, se guardan cifrados en el estado Raft y se montan en un tmpfs que nunca toca el disco; el ejercicio 3 lo comprueba dimensión por dimensión. Además, un secreto es inmutable: no se puede modificar. Rotarlo consiste en crear uno nuevo con otro nombre, actualizar el servicio con --secret-rm y --secret-add y eliminar el viejo. Es incómodo a propósito: fuerza que la rotación sea un despliegue trazable y no un cambio silencioso.
Los configs funcionan igual pero sin cifrado en reposo, y son la vía natural para init.sql, nginx.conf o prometheus.yml: ficheros de configuración que deben llegar a todos los nodos sin tener que copiarlos a mano.
- Aurora Libros en un clúster de tres nodos
# compose.swarm.yaml — stack aurora (extracto de los cuatro servicios)
services:
aurora-web:
image: nginx:alpine
ports: ["80:80"]
configs: [{ source: aurora_nginx, target: /etc/nginx/conf.d/default.conf }]
networks: [frontal]
deploy:
mode: global # una por nodo: cualquiera atiende
update_config: { parallelism: 1, order: start-first }
aurora-api:
image: ghcr.io/auroralibros/aurora-api:2.0.0
networks: [frontal, trasera]
environment:
DB_HOST: aurora-db
DB_USER: aurora
DB_NAME: aurora_libros
DB_PASSWORD_FILE: /run/secrets/db_password
REDIS_HOST: aurora-cache
secrets: [{ source: aurora_db_password, target: db_password }]
healthcheck:
test: ["CMD","node","-e","require('http').get('http://127.0.0.1:3000/salud/listo',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
interval: 10s
start_period: 20s
deploy:
replicas: 3
placement: { constraints: ["node.role==worker"] }
resources: { limits: { memory: 512M, cpus: "1.0" }, reservations: { memory: 128M } }
update_config: { parallelism: 1, delay: 15s, order: start-first, failure_action: rollback }
aurora-cache:
image: redis:7-alpine
command: ["redis-server","--maxmemory","200mb","--maxmemory-policy","allkeys-lru"]
networks: [trasera]
deploy: { replicas: 1, resources: { limits: { memory: 256M } } }
aurora-db:
image: postgres:16-alpine
environment: { POSTGRES_USER: aurora, POSTGRES_DB: aurora_libros, POSTGRES_PASSWORD_FILE: /run/secrets/db_password }
secrets: [{ source: aurora_db_password, target: db_password }]
volumes: ["aurora-datos:/var/lib/postgresql/data"]
configs: [{ source: aurora_init_sql, target: /docker-entrypoint-initdb.d/init.sql }]
networks: [trasera]
deploy:
replicas: 1
placement: { constraints: ["node.labels.datos==si"] } # anclada al nodo del volumen
update_config: { order: stop-first } # nunca dos PostgreSQL sobre los mismos datos
networks:
frontal: { driver: overlay }
trasera: { driver: overlay, internal: true, driver_opts: { encrypted: "" } }
volumes: { aurora-datos: {} }
secrets: { aurora_db_password: { external: true } }
configs:
aurora_nginx: { external: true }
aurora_init_sql: { external: true }docker node update --label-add datos=si worker-1
docker stack deploy -c compose.swarm.yaml --with-registry-auth aurora
docker stack services auroraNAME MODE REPLICAS IMAGE PORTS
aurora_aurora-api replicated 3/3 ghcr.io/auroralibros/aurora-api:2.0.0
aurora_aurora-cache replicated 1/1 redis:7-alpine
aurora_aurora-db replicated 1/1 postgres:16-alpine
aurora_aurora-web global 3/3 nginx:alpine *:80->80/tcpAdvertencia sobre los datos. La
constraintdeaurora-dbes un parche, no una solución. Los volúmenes locales no viajan: siworker-1muere, Swarm recreará la tarea de PostgreSQL en otro nodo y encontrará un volumen vacío, con lo que el catálogo desaparece. Un clúster de verdad necesita almacenamiento de red (NFS, iSCSI, Ceph, un volumen de bloque del proveedor cloud) o una base de datos gestionada fuera del clúster. Discútelo con tu responsable de infraestructura antes de poner datos reales aquí: no es un detalle de configuración, es una decisión de arquitectura.
- Swarm en 2026: cuándo sigue siendo la elección
Swarm sigue incluido en Docker Engine y mantenido, pero es minoritario: la industria estandarizó Kubernetes, y ahí están los proveedores gestionados, las herramientas y la mayor parte de la documentación.
| Situación | Elección razonable |
|---|---|
| 2-10 nodos, un equipo pequeño, sin plataforma dedicada | Swarm |
| El equipo ya domina Compose y no hay tiempo de formación | Swarm |
| Un clúster en las instalaciones del cliente, mantenido por otros | Swarm |
| Nube gestionada (EKS, GKE, AKS) disponible | Kubernetes |
| Autoescalado, operadores, service mesh, políticas, ecosistema | Kubernetes |
La comparación detallada la harás en 07-02. Lo que sí conviene retener: casi todo lo de esta lección —estado deseado, reconciliación, servicios y réplicas, red entre nodos, secretos montados como ficheros, actualizaciones progresivas— es exactamente el mismo modelo mental que vas a usar en Kubernetes, con otros nombres. Swarm no ha sido un desvío: ha sido la introducción sencilla al mismo problema.
Errores Comunes y Consejos
- Dos managers. Empeora la disponibilidad respecto a uno. Siempre 1, 3, 5 o 7.
- Olvidar
--advertise-addr. Con varias interfaces, Swarm elige mal y el clúster no se forma. Ponlo siempre explícito. - Abrir solo TCP en el cortafuegos. El 7946 necesita UDP también y el 4789 es UDP puro. El síntoma es nodos
Readycon overlay muerta. - Esperar que
build:funcione. Swarm no construye: publica la imagen antes y referénciala por etiqueta o digest. Y no olvides--with-registry-auth, o las tareas fallarán en todos los nodos menos donde hicistedocker login. - Volúmenes locales para datos. La tarea se reprograma y el volumen no la sigue. Almacenamiento de red o base de datos externa.
- Restricciones que ningún nodo cumple. El servicio se queda en
Pendingsin mensaje claro. Verifica condocker node inspect. - Consejo: usa
docker service ps --no-trunc. La columnaERRORcompleta suele contener la causa exacta del fallo. - Consejo: etiqueta los nodos desde el primer día (
zona,disco,datos). Reubicar servicios después es cuestión de una restricción.
Ejercicios
Ejercicio 1. Monta un clúster de tres nodos, despliega aurora-api con tres réplicas y demuestra la reconciliación: mata un contenedor a mano y después vacía un nodo entero, comprobando en cada caso dónde reaparecen las tareas.
Ejercicio 2. Demuestra la malla de enrutamiento: publica aurora-web con una sola réplica, comprueba que responde desde los tres nodos y averigua desde cuál se está sirviendo realmente.
Ejercicio 3. Compara un secreto de Swarm con una variable de entorno: intenta extraer ambos valores desde fuera del contenedor con docker inspect y desde dentro con /proc, y explica dónde vive físicamente cada uno.
Soluciones
Solución 1.
docker service ps aurora_aurora-api --format '{{.Name}}\t{{.Node}}\t{{.CurrentState}}'
docker kill "$(docker ps -q --filter name=aurora_aurora-api)" # ejecutado en worker-1
sleep 8
docker service ps aurora_aurora-api --format '{{.Name}}\t{{.Node}}\t{{.CurrentState}}'aurora_aurora-api.1 worker-1 Running 6 minutes ago
aurora_aurora-api.2 worker-2 Running 6 minutes ago
aurora_aurora-api.3 worker-2 Running 6 minutes ago
--- tras el kill ---
aurora_aurora-api.1 worker-1 Running 5 seconds ago
\_ aurora_aurora-api.1 worker-1 Failed 7 seconds ago "task: non-zero exit (137)"docker node update --availability drain worker-2 && sleep 15
docker service ps aurora_aurora-api --filter desired-state=running --format '{{.Name}}\t{{.Node}}'
docker service ls --filter name=aurora_aurora-api
# aurora_aurora-api.1 worker-1
# aurora_aurora-api.2 manager-1
# aurora_aurora-api.3 worker-1
# aurora_aurora-api replicated 3/3Las dos pruebas muestran la misma maquinaria a dos escalas. Al matar el contenedor, la tarea .1 se recreó en el mismo nodo, porque el nodo seguía sano y era el sitio preferido; el 137 es el SIGKILL de tu docker kill, y el historial con \_ conserva el intento fallido. Al drenar worker-2, sus dos tareas se reubicaron en los otros dos nodos, y el conteo volvió a 3/3 en unos quince segundos.
Lo importante es lo que no hiciste: no ejecutaste ningún comando de reparación. Solo declaraste "quiero tres" una vez, y el bucle de reconciliación mantiene esa afirmación cierta ante un contenedor muerto, un nodo drenado o una máquina apagada de golpe. Es el mismo mecanismo que en Kubernetes gobierna un Deployment, y por eso restart: always deja de tener sentido aquí: la política de reinicio es del servicio, no del contenedor.
Fíjate también en que la tarea .2 fue a parar a manager-1. Con la constraint node.role==worker del compose.swarm.yaml no habría podido: se habría quedado en Pending hasta que worker-2 volviera. Es el compromiso entre proteger los managers y tener sitio donde reprogramar.
Solución 2.
docker service update --replicas 1 aurora_aurora-web
docker service ps aurora_aurora-web --filter desired-state=running --format '{{.Node}}'
for n in manager-1 worker-1 worker-2; do
printf '%s -> %s\n' "$n" "$(curl -s -o /dev/null -w '%{http_code}' http://$n/salud/vivo)"
done
# worker-2
# manager-1 -> 200
# worker-1 -> 200
# worker-2 -> 200Los tres nodos responden 200 aunque solo worker-2 ejecuta la réplica. Eso es la malla de enrutamiento: al publicar en modo ingress, cada nodo abre el puerto 80 y reenvía por IPVS a través de la red overlay hasta donde estén las tareas vivas.
docker service logs aurora_aurora-web --tail 3
sudo iptables -t nat -L DOCKER-INGRESS -n | head -4
# 10.0.0.2 - - [05/Aug/2026:11:42:07] "GET /salud/vivo HTTP/1.1" 200
# DNAT tcp 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.18.0.2:80Y ahí aparece el efecto secundario que hay que conocer: la IP registrada es 10.0.0.2, una dirección de la red ingress, no la del cliente real. El DNAT de la tabla nat reescribe el destino y, por el camino, el SNAT sustituye el origen. Si Aurora Libros necesitara la IP real —para limitar peticiones por cliente o para analítica—, tendrías que publicar en mode=host, aceptando que entonces solo responderían los nodos con réplica y que el balanceador externo debe saberlo.
La ventaja compensa en la mayoría de los casos: el balanceador de entrada puede apuntar a los tres nodos sin saber nada de dónde vive cada servicio, y añadir o quitar réplicas no le obliga a cambiar su configuración.
Solución 3.
docker service create --name prueba-env --env CLAVE=ficticia-en-entorno alpine sleep 600
docker service create --name prueba-sec --secret aurora_db_password alpine sleep 600
docker service inspect prueba-env --format '{{.Spec.TaskTemplate.ContainerSpec.Env}}'
docker service inspect prueba-sec --format '{{json .Spec.TaskTemplate.ContainerSpec.Secrets}}' | jq -r '.[0].SecretName'
# [CLAVE=ficticia-en-entorno]
# aurora_db_passwordLa asimetría ya es total en la primera comprobación: la variable se lee entera en la especificación del servicio, que cualquiera con acceso a la API de Docker puede consultar; del secreto solo se ve el nombre y el punto de montaje.
cid=$(docker ps -q --filter name=prueba-sec)
docker exec "$cid" cat /run/secrets/aurora_db_password; echo
docker exec "$cid" df -h /run/secrets | tail -1
sudo tr '\0' '\n' < /proc/$(docker inspect "$cid" --format '{{.State.Pid}}')/environ | grep -c CLAVE
# clave-ficticia-aurora
# tmpfs 64M 4.0K 64M 1% /run/secrets
# 0| Dimensión | Variable de entorno | Secreto de Swarm |
|---|---|---|
En docker service inspect |
Valor completo | Solo el nombre |
En /proc/<pid>/environ |
Legible | Ausente |
| Soporte físico | Memoria del proceso | tmpfs de 64 MB (RAM) |
| En el estado del clúster | En claro en Raft | Cifrado en Raft |
| Herencia a subprocesos | Automática | Ninguna |
| Al parar la tarea | — | Se desmonta y desaparece |
El tmpfs de la segunda línea es la clave del diseño: el secreto nunca toca el disco en ningún nodo. Al parar la tarea, el sistema de ficheros se desmonta y el valor deja de existir; no queda en una capa de imagen, ni en un volumen, ni en un fichero de log, ni en un backup del sistema de ficheros del nodo.
La fila de la herencia es la que más incidentes provoca en la práctica. Una variable de entorno la ven todos los subprocesos: si tu API invoca pg_dump, un script de despliegue o cualquier herramienta de terceros, esa contraseña va con ellos, y basta con que una librería registre su entorno al fallar —cosa que hacen muchas— para que la credencial acabe en un servicio externo de gestión de errores. El fichero de /run/secrets/ solo lo lee quien decide leerlo, que es justo lo que hace tu config.js de 06-01, una vez, al arrancar.
Conclusión
Aurora Libros ya no depende de una máquina. Sabes qué aporta un orquestador —planificación, reconciliación y red entre nodos— y has montado un clúster de Swarm con docker swarm init --advertise-addr, tokens distintos por rol y los tres puertos que hay que abrir, con el detalle que cuesta tardes enteras: 7946 en TCP y UDP, 4789 en UDP. Entiendes por qué los managers deben ser impares y por qué dos son peores que uno, y manejas la rutina de drain → mantenimiento → active que permite tocar una máquina sin que nadie deje de comprar libros.
Has cambiado de modelo mental: ya no das órdenes, declaras un estado deseado y un bucle lo mantiene cierto. Lo has comprobado matando un contenedor y drenando un nodo entero, viendo cómo las tareas reaparecen solas sin que ejecutaras ninguna reparación. Distingues servicio de tarea, réplicas de modo global —el que necesitan Promtail y cAdvisor—, y sabes que una restricción imposible deja un servicio en Pending mudo. Has creado redes overlay sobre túneles VXLAN, con la decisión consciente sobre --opt encrypted para el tráfico que cruza redes que no controlas, y has visto la malla de enrutamiento responder desde los tres nodos con una sola réplica viva, junto con su precio: la IP que registra Nginx es la de la red ingress, no la del cliente.
El compose.yaml del módulo 4 se ha convertido en un stack con docker stack deploy, con la tabla de lo que Swarm ignora —build, depends_on, restart— y la sección deploy completa, donde las reservations ya no son decorativas porque gobiernan la planificación. Los secretos nativos han demostrado su ventaja de forma medible: invisibles en inspect, ausentes de /proc/<pid>/environ, montados en un tmpfs que nunca toca el disco y sin heredarse a los subprocesos, mientras el patrón _FILE de 06-01 sigue funcionando sin tocar una línea de código. Y queda anotada en rojo la limitación real: los volúmenes locales no viajan, así que aurora-db está anclada por una etiqueta de nodo y eso es un parche que hay que sustituir por almacenamiento de red o una base de datos gestionada antes de poner datos reales.
En la siguiente lección, Introducción a Kubernetes, cambias de herramienta pero no de ideas. Verás el plano de control con su kube-apiserver, su etcd y su planificador, los objetos fundamentales —Pod, Deployment, Service, Ingress, ConfigMap, Secret, PVC— con su YAML mínimo comentado, el papel central de las etiquetas y los selectores, la tabla de kubectl traducida a los comandos de Docker que ya dominas, y montarás un clúster local con kind para dar tus primeros pasos.
Docker: De Principiante a Avanzado
Módulo 1: Introducción a Docker
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
