Aurora Libros ja es construeix i es desplega sola, però continua vivint en una màquina. Si aquesta màquina s'apaga, la llibreria desapareix d'internet. Aquesta lliçó munta el teu primer clúster amb Docker Swarm: l'orquestració més propera al que ja saps, perquè parla compose.yaml i comandes docker.

Contingut

  1. Què resol un orquestrador
  2. Arquitectura de Swarm: managers, workers i Raft
  3. L'estat desitjat i el bucle de reconciliació
  4. Muntar el clúster: init, join i ports
  5. Gestionar nodes: promoure, degradar i drenar
  6. Serveis i tasques
  7. Rèpliques davant del mode global
  8. Escalat i restriccions d'ubicació
  9. Xarxes overlay: VXLAN entre nodes
  10. La malla d'encaminament
  11. Stacks: el compose.yaml que ja tens
  12. La secció deploy completa
  13. Secrets i configs natius de Swarm
  14. Aurora Libros en un clúster de tres nodes
  15. Swarm el 2026: quan continua sent l'elecció

Advertència. Un clúster multiplica les decisions de xarxa, emmagatzematge i control d'accés: els ports entre nodes, el xifratge del trànsit intern i la ubicació de les dades persistents s'han d'acordar amb el responsable d'infraestructura i de seguretat de la teva organització abans de portar res a producció.

  1. Què resol un orquestrador

Problema Compose en una màquina Orquestrador
La màquina s'apaga Tot cau Les tasques es reprogramen en altres nodes
Un contenidor mor restart: el reinicia allà mateix Es recrea on hi hagi lloc
Cal més capacitat Només si cap en aquella màquina S'afegeix un node al clúster
Publicar una versió Recrear: buit sense servei Substitució progressiva, sense tall
On col·locar cada servei? No hi ha elecció Planificador segons recursos i regles
Balancejar entre rèpliques Nginx a mà Balanceig integrat per nom de servei
Un desplegament surt malament Tornes a desplegar a mà Rollback automàtic (06-07)

Un orquestrador aporta tres coses que Compose en una sola màquina no pot donar per definició: planificació (decidir en quin node va cada cosa), reconciliació (mantenir l'estat desitjat passi el que passi) i xarxa entre màquines (que els contenidors es parlin encara que siguin en amfitrions diferents).

  1. Arquitectura de Swarm: managers, workers i Raft

flowchart TB
    subgraph P["Pla de control — Raft"]
        M1["manager-1 (líder)"] <--> M2[manager-2]
        M2 <--> M3[manager-3]
        M3 <--> M1
    end
    M1 -->|assigna tasques| W1[worker-1]
    M1 -->|assigna tasques| W2[worker-2]
    W1 --- W2
    style M1 fill:#e8f0fe,stroke:#3367d6
Rol Què fa Quants
Manager Guarda l'estat, planifica, exposa l'API, participa en Raft 1, 3, 5 o 7 (senar)
Líder El manager escollit que pren les decisions de planificació 1, escollit automàticament
Worker Només executa tasques; no pren decisions Els que calguin

L'estat del clúster —quins serveis existeixen, quantes rèpliques, quins secrets— es replica entre els managers amb l'algorisme de consens Raft. Per acceptar un canvi cal l'acord de la majoria, el quòrum, que és (N/2) + 1.

D'aquí surt la regla del nombre senar, que no és superstició sinó aritmètica:

Managers Quòrum Fallades tolerades Comentari
1 1 0 Desenvolupament. Si cau, no hi ha clúster
2 2 0 Pitjor que un: qualsevol caiguda bloqueja
3 2 1 El mínim raonable en producció
4 3 1 Igual que 3, amb més cost
5 3 2 Clústers grans

La fila dels dos managers és la que sorprèn: afegir un segon manager empitjora la disponibilitat, perquè necessites tots dos vius per assolir el quòrum. I hi ha una conseqüència important que convé tenir clara: perdre el quòrum no atura els contenidors en marxa, però deixa el clúster sense capacitat de decidir; no es pot escalar, ni desplegar, ni reprogramar res fins que tornin prou managers.

  1. L'estat desitjat i el bucle de reconciliació

Aquí hi ha el canvi mental respecte de tot l'anterior. Amb docker run dones ordres; amb un orquestrador declares com vols que estigui el món i ell se n'encarrega.

El líder executa un bucle permanent: compara l'estat desitjat (el que has declarat) amb l'estat real (el que hi ha), i si difereixen crea o elimina tasques fins que coincideixin. Quan coincideixen, espera el canvi següent i torna a comparar.

docker service create --replicas 3 aurora-api no vol dir "arrenca tres contenidors": vol dir "vull que sempre n'hi hagi tres". En mates un i n'apareix un altre. Apagues un node sencer i les seves tasques renaixen als que queden. Ningú no ha executat cap comanda de reparació: el bucle simplement ha notat una diferència entre el desitjat i el real i l'ha corregida.

  1. Muntar el clúster: init, join i ports

docker swarm init --advertise-addr 10.0.1.10        # a 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:2377

L'--advertise-addr és obligatori tan bon punt la màquina tingui més d'una interfície, i ometre'l és el primer error clàssic: Swarm tria una IP qualsevol —sovint la pública, o una d'una VPN— i els altres nodes no aconsegueixen arribar-hi.

docker swarm join-token worker             # els tokens són diferents per rol
docker swarm join-token --rotate manager   # si un token s'ha filtrat
docker swarm join --token SWMTKN-1-49nj1...-8vxv8 10.0.1.10:2377   # a cada worker
docker node ls                             # des de qualsevol 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
Port Protocol Per a què Entre
2377 TCP API de gestió del clúster Nodes → managers
7946 TCP i UDP Descobriment i gossip entre nodes Tots ↔ tots
4789 UDP Trànsit de dades VXLAN de les xarxes overlay Tots ↔ tots

Aquests tres ports causen la meitat dels problemes d'un clúster nou, i sempre pel mateix: 7946 necessita TCP i UDP, i 4789 és UDP. Un tallafocs que només obri TCP produeix el símptoma més desconcertant possible: els nodes apareixen Ready, els serveis es despleguen, però els contenidors de nodes diferents no es veuen entre ells. No obris mai el 2377 a internet: qui hi arribi amb un token es pot unir al teu clúster.

  1. Gestionar nodes: promoure, degradar i 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 disc=ssd worker-1
Availability Tasques noves Tasques existents Quan
active Continuen Normal
pause No Continuen Investigar sense que n'arribin més
drain No Es mouen a altres nodes Manteniment o retirada
docker node update --availability drain worker-1     # buidar abans de tocar la màquina
# ... manteniment, reinici, actualització del kernel ...
docker node update --availability active worker-1

El cicle drain → manteniment → active és la rutina bàsica d'operació d'un clúster: les tasques es recol·loquen soles abans que toquis res, i ningú no es queda sense servei. Compte amb un detall: en tornar a active, les tasques no tornen soles al node; es queden on són fins al desplegament següent.

  1. Serveis i tasques

Swarm introdueix dos conceptes nous sobre el que ja coneixes:

  • Servei: la declaració. "Vull tres rèpliques d'aquesta imatge amb aquesta configuració".
  • Tasca: cada unitat de treball assignada a un node. Una tasca acaba sent un contenidor, i és immutable: no es modifica ni es reinicia, se substitueix per una de nova.
docker service create --name aurora-api --replicas 3 \
  --network aurora-posterior --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
Comanda Què mostra Equivalent que ja coneixes
docker service ls Serveis i rèpliques preparades docker compose ps
docker service ps <svc> Cada tasca, el seu node i el seu historial docker ps per servei
docker service logs -f <svc> Logs agregats de totes les rèpliques docker compose logs -f
docker service inspect <svc> Especificació completa docker inspect
docker service update <svc> Canvia l'estat desitjat Editar i up -d
docker service rm <svc> Elimina el servei i les seves tasques docker compose rm

docker service ps és l'eina de diagnòstic principal, perquè mostra també les tasques mortes i per què van morir:

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)"

Aquesta última línia explica una història completa: la tasca 3 va morir a worker-1 amb el codi 78 que vas programar a 06-01, és a dir, configuració invàlida, i l'orquestrador la va recrear a worker-2. Sense la validació d'arrencada, aquí hi hauria un genèric exit (1).

  1. Rèpliques davant del mode global

Mode Quantes tasques En afegir un node Per a què
--mode replicated (per defecte) Les que demanis No canvia Aplicacions: aurora-api, aurora-web
--mode global Una per node, sempre N'apareix una de nova Agents: Promtail, cAdvisor, node-exporter
--mode replicated-job N execucions i acaba Migracions, tasques puntuals

El mode global és exactament el que necessiten els recol·lectors de mètriques i logs del mòdul 5: cada node necessita el seu cAdvisor i el seu Promtail, i vols que apareguin sols a cada màquina que afegeixis al clúster sense recordar-te'n.

  1. Escalat i restriccions d'ubicació

docker service scale aurora-api=5 aurora-web=3        # diversos alhora
docker service update --replicas 2 aurora-api         # equivalent

# Restriccions DURES: si no es compleixen, la tasca no es programa (queda Pending)
docker service update --constraint-add 'node.labels.disc==ssd' aurora-db
# Preferències TOVES: reparteixen, però no impedeixen
docker service update --placement-pref 'spread=node.labels.zona' aurora-api
Expressió Significat
node.role==worker Només en workers (manté els managers descarregats)
node.labels.zona==a Només en nodes amb aquesta etiqueta
node.hostname!=worker-2 En qualsevol menys aquest
spread=node.labels.zona Repartiment equitatiu entre zones

La diferència entre restricció i preferència importa molt a la pràctica: una restricció impossible deixa el servei en Pending per sempre, sense cap error evident, mentre que una preferència només influeix en el repartiment. Si un servei no arrenca i docker service ps no mostra cap error, sospita d'una restricció que cap node no compleix.

  1. Xarxes overlay: VXLAN entre nodes

La xarxa bridge de 03-05 només connecta contenidors del mateix amfitrió. Una xarxa overlay connecta contenidors de nodes diferents com si compartissin 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-posterior

Per dins és un túnel VXLAN: cada paquet Ethernet del contenidor s'encapsula dins d'un datagrama UDP cap al port 4789 del node destí, que el desencapsula i l'entrega. Per als contenidors, l'existència de dues màquines és invisible; continuen resolent aurora-db per DNS i parlant per la seva IP de la xarxa overlay.

Opció Efecte Cost
--attachable Permet connectar contenidors solts, no només serveis Cap
--opt encrypted Xifra amb IPsec el trànsit entre nodes 10-30 % de rendiment
--internal Sense sortida a internet Cap
--subnet Rang fix, evita col·lisions amb la teva xarxa corporativa Cap

El --opt encrypted mereix una decisió conscient. El trànsit VXLAN viatja en clar per defecte: si els teus nodes són en una xarxa privada d'un mateix centre de dades pot ser acceptable, però si travessen internet o una xarxa compartida, el catàleg, les consultes i les credencials que hi passin són llegibles per qualsevol amb accés al medi. És exactament el tipus de decisió que has de validar amb el teu responsable de seguretat. Compte també: el xifratge s'aplica al pla de dades, i no es pot activar ni desactivar sense recrear la xarxa.

  1. La malla d'encaminament

flowchart TB
    C[Client] -->|:8080| N2["worker-2<br/>(sense rèplica local)"]
    N2 -->|IPVS per la overlay| T1["tasca 1 @ worker-1"]
    N2 --> T2["tasca 2 @ manager-1"]
    style N2 fill:#fff4e5,stroke:#e8a33d

Quan publiques un port en mode ingress (el predeterminat), tots els nodes del clúster hi escolten, fins i tot els que no executen cap rèplica d'aquell servei. El node que rep la connexió la reparteix per IPVS entre les tasques vives, siguin on siguin.

curl http://worker-2:8080/salut/viu    # funciona encara que worker-2 no tingui rèpliques

L'avantatge és enorme per al balancejador d'entrada: apunta a qualsevol node, o a tots, i no li cal saber on és res. Els inconvenients són dos i convé conèixer-los: hi ha un salt de xarxa extra quan el node que rep no té la tasca, i la IP d'origen que veu la teva aplicació és la del node, no la del client real, perquè hi ha SNAT pel mig.

Mode de publicació Sintaxi Comportament
ingress (per defecte) --publish 8080:3000 Tots els nodes escolten; balanceig per IPVS
host --publish mode=host,published=8080,target=3000 Només els nodes amb rèplica; IP real del client, sense salt extra

El mode host és la sortida quan necessites la IP del client (registre d'accés, límits per IP, geolocalització) o l'últim gram de latència, i té un preu: obliga el balancejador extern a saber en quins nodes hi ha rèpliques, i no pots tenir dues rèpliques del mateix servei en un node, perquè xocarien al port.

  1. Stacks: el compose.yaml que ja tens

Aquí arriba la recompensa de tot el mòdul 4: docker stack deploy accepta el teu 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 aurora

El --with-registry-auth és imprescindible amb un registre d'imatges privat com ghcr.io: sense ell, el manager entén les credencials però no les reenvia als workers, i les tasques fallen amb no basic auth credentials a tots els nodes menys on vas fer docker login.

Clau de Compose A Swarm
image, environment, networks, ports, secrets, configs, healthcheck Funcionen igual
deploy: Només aquí té efecte (Compose la ignorava llevat de resources)
build: Ignorada: cal publicar la imatge en un registre d'imatges
depends_on: Ignorada: no hi ha ordre d'arrencada, hi ha reintents
restart:, container_name, develop, profiles Ignorades
volumes: amb ruta relativa Perillosa: es resol a cada node

Les dues ignorades del mig canvien com dissenyes l'aplicació. Sense build, el flux obliga a passar pel registre d'imatges, cosa que és correcta en producció. I sense depends_on, la teva API ha de suportar arrencar abans que la base de dades: per això vas implementar el reintent amb backoff a 04-04, i per això les sondes de 06-01 retornen 503 mentre les dependències no hi siguin.

  1. La secció deploy completa

services:
  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Í que les respecta
      restart_policy:
        condition: on-failure      # any | on-failure | none
        delay: 5s
        max_attempts: 3
        window: 120s
      update_config:               # es detalla a 06-07
        parallelism: 1
        delay: 10s
        order: start-first
        failure_action: rollback
      rollback_config: { parallelism: 2, order: stop-first }
      labels: { com.aurora.component: api }

Un matís que separa reservations de limits i que a Compose no existia: a Swarm, les reserves afecten la planificació. El planificador suma les reserves de les tasques ja ubicades a cada node i només en col·loca una de nova on càpiga. Reservar de més deixa nodes buits amb serveis en Pending; reservar de menys amuntega tasques fins que el node es queda sense memòria. Els números mesurats de 06-01 són just el que necessites aquí.

  1. Secrets i configs natius de Swarm

printf 'clau-ficticia-aurora' | docker secret create aurora_db_password -
docker config create aurora_init_sql ./db/init.sql
docker secret ls && docker config ls
services:
  aurora-api:
    secrets:
      - source: aurora_db_password
        target: db_password        # queda a /run/secrets/db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password    # el patró _FILE de 06-01, intacte
secrets:
  aurora_db_password:
    external: true
configs:
  aurora_init_sql:
    external: true

Els secrets viatgen als nodes xifrats per TLS mutu, es desen xifrats a l'estat Raft i es munten en un tmpfs que no toca mai el disc; l'exercici 3 ho comprova dimensió per dimensió. A més, un secret és immutable: no es pot modificar. Rotar-lo consisteix a crear-ne un de nou amb un altre nom, actualitzar el servei amb --secret-rm i --secret-add i eliminar el vell. És incòmode expressament: força que la rotació sigui un desplegament traçable i no un canvi silenciós.

Els configs funcionen igual però sense xifratge en repòs, i són la via natural per a init.sql, nginx.conf o prometheus.yml: fitxers de configuració que han d'arribar a tots els nodes sense haver-los de copiar a mà.

  1. Aurora Libros en un clúster de tres nodes

# compose.swarm.yaml — stack aurora (extracte dels quatre serveis)
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 per node: qualsevol atén
      update_config: { parallelism: 1, order: start-first }

  aurora-api:
    image: ghcr.io/auroralibros/aurora-api:2.0.0
    networks: [frontal, posterior]
    environment:
      DB_HOST: aurora-db
      DB_USER: aurora
      DB_NAME: aurora_llibres
      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/salut/preparat',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: [posterior]
    deploy: { replicas: 1, resources: { limits: { memory: 256M } } }

  aurora-db:
    image: postgres:16-alpine
    environment: { POSTGRES_USER: aurora, POSTGRES_DB: aurora_llibres, POSTGRES_PASSWORD_FILE: /run/secrets/db_password }
    secrets: [{ source: aurora_db_password, target: db_password }]
    volumes: ["aurora-dades:/var/lib/postgresql/data"]
    configs: [{ source: aurora_init_sql, target: /docker-entrypoint-initdb.d/init.sql }]
    networks: [posterior]
    deploy:
      replicas: 1
      placement: { constraints: ["node.labels.dades==si"] }  # ancorada al node del volum
      update_config: { order: stop-first }   # mai dos PostgreSQL sobre les mateixes dades

networks:
  frontal: { driver: overlay }
  posterior: { driver: overlay, internal: true, driver_opts: { encrypted: "" } }
volumes: { aurora-dades: {} }
secrets: { aurora_db_password: { external: true } }
configs:
  aurora_nginx:    { external: true }
  aurora_init_sql: { external: true }
docker node update --label-add dades=si worker-1
docker stack deploy -c compose.swarm.yaml --with-registry-auth aurora
docker stack services aurora
NAME                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/tcp

Advertència sobre les dades. La constraint d'aurora-db és un pedaç, no una solució. Els volums locals no viatgen: si worker-1 mor, Swarm recrearà la tasca de PostgreSQL en un altre node i hi trobarà un volum buit, amb la qual cosa el catàleg desapareix. Un clúster de debò necessita emmagatzematge de xarxa (NFS, iSCSI, Ceph, un volum de bloc del proveïdor de núvol) o una base de dades gestionada fora del clúster. Discuteix-ho amb el teu responsable d'infraestructura abans de posar-hi dades reals: no és un detall de configuració, és una decisió d'arquitectura.

  1. Swarm el 2026: quan continua sent l'elecció

Swarm continua inclòs a Docker Engine i mantingut, però és minoritari: la indústria va estandarditzar Kubernetes, i allà hi ha els proveïdors gestionats, les eines i la major part de la documentació.

Situació Elecció raonable
2-10 nodes, un equip petit, sense plataforma dedicada Swarm
L'equip ja domina Compose i no hi ha temps de formació Swarm
Un clúster a les instal·lacions del client, mantingut per altres Swarm
Núvol gestionat (EKS, GKE, AKS) disponible Kubernetes
Autoescalat, operadors, service mesh, polítiques, ecosistema Kubernetes

La comparació detallada la faràs a 07-02. El que sí que convé retenir: gairebé tot el d'aquesta lliçó —estat desitjat, reconciliació, serveis i rèpliques, xarxa entre nodes, secrets muntats com a fitxers, actualitzacions progressives— és exactament el mateix model mental que faràs servir a Kubernetes, amb altres noms. Swarm no ha estat una desviació: ha estat la introducció senzilla al mateix problema.

Errors Habituals i Consells

  • Dos managers. Empitjora la disponibilitat respecte d'un. Sempre 1, 3, 5 o 7.
  • Oblidar --advertise-addr. Amb diverses interfícies, Swarm tria malament i el clúster no es forma. Posa-ho sempre explícit.
  • Obrir només TCP al tallafocs. El 7946 necessita UDP també i el 4789 és UDP pur. El símptoma són nodes Ready amb la overlay morta.
  • Esperar que build: funcioni. Swarm no construeix: publica la imatge abans i referencia-la per etiqueta o digest. I no oblidis --with-registry-auth, o les tasques fallaran a tots els nodes menys on vas fer docker login.
  • Volums locals per a dades. La tasca es reprograma i el volum no la segueix. Emmagatzematge de xarxa o base de dades externa.
  • Restriccions que cap node no compleix. El servei es queda en Pending sense missatge clar. Verifica-ho amb docker node inspect.
  • Consell: fes servir docker service ps --no-trunc. La columna ERROR completa sol contenir la causa exacta de la fallada.
  • Consell: etiqueta els nodes des del primer dia (zona, disc, dades). Reubicar serveis després és qüestió d'una restricció.

Exercicis

Exercici 1. Munta un clúster de tres nodes, desplega aurora-api amb tres rèpliques i demostra la reconciliació: mata un contenidor a mà i després buida un node sencer, comprovant en cada cas on reapareixen les tasques.

Exercici 2. Demostra la malla d'encaminament: publica aurora-web amb una sola rèplica, comprova que respon des dels tres nodes i esbrina des de quin s'està servint realment.

Exercici 3. Compara un secret de Swarm amb una variable d'entorn: intenta extreure'n els dos valors des de fora del contenidor amb docker inspect i des de dins amb /proc, i explica on viu físicament cadascun.

Solucions

Solució 1.

docker service ps aurora_aurora-api --format '{{.Name}}\t{{.Node}}\t{{.CurrentState}}'
docker kill "$(docker ps -q --filter name=aurora_aurora-api)"     # executat a 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
--- després del 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/3

Les dues proves mostren la mateixa maquinària a dues escales. En matar el contenidor, la tasca .1 es va recrear al mateix node, perquè el node continuava sa i era el lloc preferit; el 137 és el SIGKILL del teu docker kill, i l'historial amb \_ conserva l'intent fallit. En drenar worker-2, les seves dues tasques es van reubicar als altres dos nodes, i el recompte va tornar a 3/3 en uns quinze segons.

L'important és el que no vas fer: no vas executar cap comanda de reparació. Només vas declarar "en vull tres" una vegada, i el bucle de reconciliació manté aquella afirmació certa davant d'un contenidor mort, un node drenat o una màquina apagada de cop. És el mateix mecanisme que a Kubernetes governa un Deployment, i per això restart: always deixa de tenir sentit aquí: la política de reinici és del servei, no del contenidor.

Fixa't també que la tasca .2 va anar a parar a manager-1. Amb la constraint node.role==worker del compose.swarm.yaml no hi hauria pogut anar: s'hauria quedat en Pending fins que worker-2 tornés. És el compromís entre protegir els managers i tenir lloc on reprogramar.

Solució 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/salut/viu)"
done
# worker-2
# manager-1 -> 200
# worker-1  -> 200
# worker-2  -> 200

Els tres nodes responen 200 encara que només worker-2 executa la rèplica. Això és la malla d'encaminament: en publicar en mode ingress, cada node obre el port 80 i reenvia per IPVS a través de la xarxa overlay fins on siguin les tasques vives.

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 /salut/viu HTTP/1.1" 200
# DNAT  tcp  0.0.0.0/0  0.0.0.0/0  tcp dpt:80 to:172.18.0.2:80

I aquí apareix l'efecte secundari que cal conèixer: la IP registrada és 10.0.0.2, una adreça de la xarxa ingress, no la del client real. El DNAT de la taula nat reescriu el destí i, pel camí, l'SNAT substitueix l'origen. Si Aurora Libros necessités la IP real —per limitar peticions per client o per a analítica—, hauries de publicar en mode=host, acceptant que llavors només respondrien els nodes amb rèplica i que el balancejador extern ho ha de saber.

L'avantatge compensa en la majoria dels casos: el balancejador d'entrada pot apuntar als tres nodes sense saber res d'on viu cada servei, i afegir o treure rèpliques no l'obliga a canviar la seva configuració.

Solució 3.

docker service create --name prova-env --env CLAU=ficticia-a-entorn alpine sleep 600
docker service create --name prova-sec --secret aurora_db_password alpine sleep 600

docker service inspect prova-env --format '{{.Spec.TaskTemplate.ContainerSpec.Env}}'
docker service inspect prova-sec --format '{{json .Spec.TaskTemplate.ContainerSpec.Secrets}}' | jq -r '.[0].SecretName'
# [CLAU=ficticia-a-entorn]
# aurora_db_password

L'asimetria ja és total a la primera comprovació: la variable es llegeix sencera a l'especificació del servei, que qualsevol amb accés a l'API de Docker pot consultar; del secret només se'n veu el nom i el punt de muntatge.

cid=$(docker ps -q --filter name=prova-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 CLAU
# clau-ficticia-aurora
# tmpfs   64M  4.0K  64M   1%  /run/secrets
# 0
Dimensió Variable d'entorn Secret de Swarm
A docker service inspect Valor complet Només el nom
A /proc/<pid>/environ Llegible Absent
Suport físic Memòria del procés tmpfs de 64 MB (RAM)
A l'estat del clúster En clar a Raft Xifrat a Raft
Herència a subprocessos Automàtica Cap
En aturar la tasca Es desmunta i desapareix

El tmpfs de la segona línia és la clau del disseny: el secret no toca mai el disc en cap node. En aturar la tasca, el sistema de fitxers es desmunta i el valor deixa d'existir; no queda en cap capa d'imatge, ni en un volum, ni en un fitxer de log, ni en una còpia de seguretat del sistema de fitxers del node.

La fila de l'herència és la que més incidents provoca a la pràctica. Una variable d'entorn la veuen tots els subprocessos: si la teva API invoca pg_dump, un script de desplegament o qualsevol eina de tercers, aquesta contrasenya hi va amb ells, i n'hi ha prou que una llibreria registri el seu entorn en fallar —cosa que fan moltes— perquè la credencial acabi en un servei extern de gestió d'errors. El fitxer de /run/secrets/ només el llegeix qui decideix llegir-lo, que és justament el que fa el teu config.js de 06-01, una vegada, en arrencar.

Conclusió

Aurora Libros ja no depèn d'una màquina. Saps què aporta un orquestrador —planificació, reconciliació i xarxa entre nodes— i has muntat un clúster de Swarm amb docker swarm init --advertise-addr, tokens diferents per rol i els tres ports que cal obrir, amb el detall que costa tardes senceres: 7946 en TCP i UDP, 4789 en UDP. Entens per què els managers han de ser senars i per què dos són pitjor que un, i domines la rutina de drain → manteniment → active que permet tocar una màquina sense que ningú deixi de comprar llibres.

Has canviat de model mental: ja no dones ordres, declares un estat desitjat i un bucle el manté cert. Ho has comprovat matant un contenidor i drenant un node sencer, veient com les tasques reapareixen soles sense que executessis cap reparació. Distingeixes servei de tasca, rèpliques de mode global —el que necessiten Promtail i cAdvisor—, i saps que una restricció impossible deixa un servei en Pending mut. Has creat xarxes overlay sobre túnels VXLAN, amb la decisió conscient sobre --opt encrypted per al trànsit que travessa xarxes que no controles, i has vist la malla d'encaminament respondre des dels tres nodes amb una sola rèplica viva, juntament amb el seu preu: la IP que registra Nginx és la de la xarxa ingress, no la del client.

El compose.yaml del mòdul 4 s'ha convertit en un stack amb docker stack deploy, amb la taula del que Swarm ignora —build, depends_on, restart— i la secció deploy completa, on les reservations ja no són decoratives perquè governen la planificació. Els secrets natius han demostrat el seu avantatge de manera mesurable: invisibles a inspect, absents de /proc/<pid>/environ, muntats en un tmpfs que no toca mai el disc i sense heretar-se als subprocessos, mentre el patró _FILE de 06-01 continua funcionant sense tocar ni una línia de codi. I queda anotada en vermell la limitació real: els volums locals no viatgen, així que aurora-db està ancorada per una etiqueta de node i això és un pedaç que cal substituir per emmagatzematge de xarxa o una base de dades gestionada abans de posar-hi dades reals.

A la lliçó següent, Introducció a Kubernetes, canvies d'eina però no d'idees. Veuràs el pla de control amb el seu kube-apiserver, el seu etcd i el seu planificador, els objectes fonamentals —Pod, Deployment, Service, Ingress, ConfigMap, Secret, PVC— amb el seu YAML mínim comentat, el paper central de les etiquetes i els selectors, la taula de kubectl traduïda a les comandes de Docker que ja domines, i muntaràs un clúster local amb kind per fer els teus primers passos.

Docker: De Principiant a Avançat

Mòdul 1: Introducció a Docker

Mòdul 2: Treballant amb Imatges Docker

Mòdul 3: Contenidors Docker

Mòdul 4: Docker Compose

Mòdul 5: Conceptes Avançats de Docker

Mòdul 6: Docker en Producció

Mòdul 7: Ecosistema i Eines de Docker

© Copyright 2026. Tots els drets reservats