En tancar el mòdul 4 teníem servei-cataleg i servei-comandes funcionant amb npm run dev en un portàtil, amb MongoDB, PostgreSQL i RabbitMQ aixecats a mà amb docker run. Això serveix per desenvolupar, però no per desplegar: en producció cada servei s'executarà en diverses rèpliques, en màquines que ningú no ha preparat a mà, i haurà d'arrencar, morir i tornar a arrencar centenars de vegades sense que cap humà hi intervingui. El contenidor és la unitat que ho fa possible: empaqueta el servei amb el seu Node.js exacte i les seves dependències en una imatge immutable que s'executa igual al portàtil del Luis, al runner de CI i al clúster de Kubernetes. Aquesta lliçó construeix la imatge de producció de servei-cataleg, explica com s'etiqueta, s'executa i s'atura, i aixeca per primera vegada el sistema complet de TechCorp amb Docker Compose, inclosa l'única prova E2E que 04-05 va deixar pendent. Kubernetes (05-02) i el pipeline que construirà aquestes imatges a cada commit (05-03) es recolzen en el que aquí queda fixat.

Contingut

  1. Per què contenidors per a microserveis
  2. Conceptes: imatge, capa, contenidor, registre, volum i xarxa
  3. El Dockerfile de producció de servei-cataleg, línia a línia
  4. .dockerignore, memòria cau de capes i ordre de les instruccions
  5. Etiquetatge d'imatges i registre
  6. Ordres bàsiques de Docker
  7. Senyals, PID 1 i aturada ordenada dins del contenidor
  8. Docker Compose: l'entorn local complet de TechCorp
  9. Operar l'entorn: up, down, logs, ps, migracions i la prova E2E
  10. Mida d'imatge i seguretat bàsica

  1. Per què contenidors per a microserveis

Un contenidor és un procés aïllat que el nucli de Linux executa amb el seu propi sistema de fitxers, la seva pròpia vista de la xarxa i límits de CPU i memòria, a partir d'una imatge que conté tot el que aquell procés necessita (binari de Node, node_modules, codi). No hi ha cap sistema operatiu convidat ni cap hipervisor: el contenidor comparteix el nucli de l'amfitrió, i per això arrenca en mil·lisegons i ocupa megabytes.

Aspecte Màquina virtual Contenidor
Què es virtualitza Maquinari complet (CPU, disc, xarxa) amb el seu propi nucli Només l'espai d'usuari; comparteix el nucli de l'amfitrió
Mida típica Gigabytes Desenes o centenars de megabytes
Arrencada De desenes de segons a minuts De mil·lisegons a segons
Densitat per màquina Unitats o desenes Centenars
Aïllament Fort (nucli propi) Bo (namespaces i cgroups), no equivalent a una VM
Unitat de desplegament Una imatge de VM (AMI, OVA), feixuga de construir Una imatge de contenidor, construïda en segons per CI

Per a TechCorp l'encaix és directe:

  • Una imatge = una unitat de desplegament. servei-cataleg és una imatge; desplegar la versió 1.4.2 és executar aquesta imatge. No hi ha "instal·lar Node 20 al servidor" ni "copiar la carpeta i fer npm install": tot això va passar una vegada, en construir la imatge.
  • Reproduïbilitat. La mateixa imatge que va passar les proves d'integració a CI és la que s'executa en producció, byte a byte. S'acaba el "a la meva màquina funciona" i el "a producció hi ha una altra versió de pg".
  • Aïllament. Sis serveis Node a la mateixa màquina no comparteixen node_modules, ni ports, ni variables d'entorn. Cadascun es pensa que té la màquina per a ell sol.
  • Arrencada ràpida i d'un sol ús. L'aturada ordenada de 04-02 i la configuració per entorn de 04-03 es van escriure pensant en això: un contenidor neix, serveix, rep SIGTERM i mor; un altre n'ocupa el lloc.

  1. Conceptes: imatge, capa, contenidor, registre, volum i xarxa

Concepte Què és A TechCorp
Imatge Plantilla immutable, només de lectura, amb el sistema de fitxers i metadades (ordre d'arrencada, ports, usuari) ghcr.io/techcorp/servei-cataleg:1.4.2
Capa Cada instrucció del Dockerfile que canvia el sistema de fitxers produeix una capa; la imatge és la pila de capes. Les capes es guarden a la memòria cau i es comparteixen entre imatges Les sis imatges de serveis comparteixen la capa base de node:20-alpine
Contenidor Una instància en execució d'una imatge: les capes de la imatge més una capa d'escriptura efímera Cada rèplica de servei-cataleg
Registre Magatzem d'imatges des del qual es fa push/pull GitHub Container Registry (ghcr.io), al costat del codi i de @techcorp/comu-http (04-01)
Volum Emmagatzematge persistent fora de la capa d'escriptura del contenidor Dades de PostgreSQL, MongoDB i RabbitMQ en local; els serveis de TechCorp no fan servir volums (són stateless)
Xarxa Xarxa virtual en què els contenidors es resolen pel nom A Compose, servei-comandes crida http://servei-cataleg:3001, el mateix nom que resoldrà el DNS de Kubernetes (03-05)

Que els serveis no tinguin estat a disc no és casualitat: és el que permet matar-los i replicar-los sense pensar-hi. Tot el que ha de sobreviure viu a la base de dades o al broker.

  1. El Dockerfile de producció de servei-cataleg, línia a línia

El Dockerfile viu a l'arrel del repositori techcorp/servei-cataleg i prové de la plantilla techcorp/plantilla-servei-node (04-01, exercici 2: el Dockerfile es copia i s'adapta). Fa servir multi-stage build: una etapa instal·la dependències i una altra, neta, es queda només amb el que cal per executar.

# syntax=docker/dockerfile:1

# ---------- Etapa 1: dependències ----------
FROM node:20-alpine AS dependencies
WORKDIR /app
# Només els fitxers que defineixen les dependències: si no canvien, aquesta capa (i npm ci) es reutilitzen de la memòria cau.
COPY package.json package-lock.json ./
# @techcorp/comu-http és a GitHub Packages (04-01): npm necessita un token per descarregar-la.
# El .npmrc es munta NOMÉS durant aquest RUN com a secret de build; no queda en cap capa de la imatge.
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci --omit=dev

# ---------- Etapa 2: imatge final ----------
FROM node:20-alpine
ENV NODE_ENV=production
WORKDIR /app
# node_modules ja instal·lat i sense dependències de desenvolupament; propietat de l'usuari 'node' que porta la imatge base.
COPY --from=dependencies --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node package.json ./
COPY --chown=node:node src ./src
COPY --chown=node:node scripts ./scripts
# A partir d'aquí el procés no és root.
USER node
EXPOSE 3001
# Opcional: Docker (no Kubernetes) marca el contenidor com a unhealthy si /health/live no respon 200.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:3001/health/live || exit 1
# Forma exec (JSON): node és PID 1 i rep SIGTERM directament (apartat 7).
CMD ["node", "src/servidor.js"]

Instrucció per instrucció:

  • # syntax=docker/dockerfile:1: activa la sintaxi moderna de BuildKit (necessària per a --mount=type=secret).
  • FROM node:20-alpine AS dependencies: imatge base oficial de Node 20 sobre Alpine Linux (uns 50 MB davant dels 350 MB de node:20). AS dependencies dona nom a l'etapa per poder copiar-ne coses després.
  • WORKDIR /app: crea /app i el converteix en directori de treball per a les instruccions següents.
  • COPY package.json package-lock.json ./ abans de copiar el codi: és la clau de la memòria cau (apartat 4).
  • RUN --mount=type=secret,id=npmrc ... npm ci --omit=dev: npm ci instal·la exactament el que diu el package-lock.json (reproduïble; falla si el lock no quadra amb package.json); --omit=dev deixa fora nodemon, jest, supertest, @pact-foundation/pact, testcontainers. El token de GitHub Packages es munta com a fitxer només durant aquesta ordre; es passa en construir amb --secret id=npmrc,src=$HOME/.npmrc.
  • FROM node:20-alpine (segona vegada): la imatge final comença de zero; res de l'etapa anterior no hi passa tret del que es copiï explícitament.
  • ENV NODE_ENV=production: Express desactiva missatges de depuració i algunes llibreries optimitzen; el config.js de 04-03 el llegeix com una variable més.
  • COPY --from=dependencies --chown=node:node /app/node_modules ./node_modules: porta només node_modules ja instal·lat. --chown=node:node assigna la propietat a l'usuari node (uid 1000) que la imatge base ja defineix; sense això els fitxers serien de root i el procés no root podria no poder-los llegir si els permisos fossin restrictius.
  • Tres COPY separats per a package.json, src i scripts: scripts/ hi entra perquè llavor.js s'executa des de la mateixa imatge (apartat 8). No es copien proves, contractes ni fitxers de configuració de desenvolupament.
  • USER node: tot el que s'executi a partir d'aquí (inclòs CMD) corre sense privilegis. És la mesura de seguretat més barata i més eficaç d'aquesta lliçó.
  • EXPOSE 3001: documenta el port (no el publica; això es fa amb -p o a Compose/Kubernetes). Coincideix amb PORT=3001 de 04-02.
  • HEALTHCHECK: Docker executa wget contra /health/live cada 30 s, amb 10 s de gràcia en arrencar; tres fallades seguides marquen el contenidor unhealthy. Alpine porta wget a BusyBox, així que no cal instal·lar curl. Kubernetes ignora HEALTHCHECK i fa servir les seves pròpies probes (05-02); a Compose sí que és útil per a depends_on (apartat 8).
  • CMD ["node", "src/servidor.js"]: l'ordre d'arrencada, igual que l'script start de 04-02, però sense passar per npm (apartat 7).

Per què multi-stage si un servei JavaScript no compila res? Perquè separa dues preocupacions: a la primera etapa hi pot haver tokens, memòria cau d'npm, eines de compilació de mòduls natius; a la segona, només el que s'executa. Si un dia un servei s'escriu en TypeScript, l'etapa 1 passa a ser "instal·lar i compilar" i l'etapa 2 no canvia.

  1. .dockerignore, memòria cau de capes i ordre de les instruccions

COPY src ./src copia el que hi ha al context de build (el directori que es passa a docker build). Sense filtre, un COPY . . arrossegaria el node_modules local (amb dependències de desenvolupament i binaris d'un altre sistema operatiu), .env amb secrets, .git, pactes/, cobertura de proves... El .dockerignore els exclou del context:

node_modules
npm-debug.log
.env
.env.*
!.env.exemple
.git
.github
coverage
proves
pactes
*.md

Sobre la memòria cau: Docker construeix les capes en ordre i, per a cada instrucció, reutilitza la capa desada si la instrucció i les seves entrades no han canviat; tan bon punt una capa canvia, totes les següents es reconstrueixen. D'aquí l'ordre del Dockerfile:

flowchart LR
    A[FROM node:20-alpine] --> B[COPY package*.json]
    B --> C[RUN npm ci]
    C --> D[COPY src, scripts]
    D --> E[CMD]
    style C fill:#dfe,stroke:#393
    style D fill:#fdd,stroke:#933

Un canvi a src/rutes/productes.js invalida només la capa vermella: l'npm ci (l'operació lenta, 30-60 s amb descàrrega) es reutilitza de la memòria cau. Si copiéssim primer tot el codi i després instal·léssim, cada commit repetiria la instal·lació. Regla: el que canvia menys, a dalt; el que canvia més, a baix.

  1. Etiquetatge d'imatges i registre

Una imatge s'identifica per registre/organització/nom:etiqueta. TechCorp fa servir dues etiquetes per build:

Etiqueta Exemple Qui la fa servir
Versió semàntica ghcr.io/techcorp/servei-cataleg:1.4.2 Manifestos de Kubernetes, notes de versió, humans
Commit d'origen ghcr.io/techcorp/servei-cataleg:sha-9f3c2ab Traçabilitat exacta: de quin codi surt la imatge; el pipeline de 05-03 la crea a cada build

Totes dues apunten al mateix digest (sha256:...), que és l'identificador real i immutable de la imatge. El que no es fa servir en producció és latest:

  • latest no vol dir "la més recent", vol dir "l'última a la qual algú va posar aquesta etiqueta". És una etiqueta mutable: avui apunta a 1.4.2 i demà a 1.5.0 sense que cap manifest canviï.
  • Amb latest, dues rèpliques del mateix Deployment poden executar versions diferents segons quan van fer pull, i un rollback és impossible perquè ningú no sap què hi havia abans.
  • Els manifestos de 05-02 porten sempre una etiqueta concreta; canviar de versió és canviar aquella línia, i això queda a git.

Construcció i publicació manual (el pipeline de 05-03 automatitza exactament això):

# A l'arrel de servei-cataleg. -t afegeix una etiqueta; se'n poden posar diverses.
docker build \
  --secret id=npmrc,src=$HOME/.npmrc \
  -t ghcr.io/techcorp/servei-cataleg:1.4.2 \
  -t ghcr.io/techcorp/servei-cataleg:sha-9f3c2ab \
  .

# Autenticació al registre amb un token de GitHub amb permís write:packages
echo "$GITHUB_TOKEN" | docker login ghcr.io -u luis --password-stdin

docker push ghcr.io/techcorp/servei-cataleg:1.4.2
docker push ghcr.io/techcorp/servei-cataleg:sha-9f3c2ab

  1. Ordres bàsiques de Docker

Les que es fan servir cada dia, amb la imatge acabada de construir:

# Executar en segon pla (-d), amb nom, publicant el port 3001 del contenidor al 3001 de l'amfitrió (-p host:contenidor)
# i passant la configuració per variables d'entorn (04-03). --rm esborra el contenidor en aturar-se.
docker run -d --rm --name cataleg -p 3001:3001 \
  -e MONGO_URL=mongodb://host.docker.internal:27017 -e MONGO_BD=cataleg -e LOG_NIVELL=debug \
  ghcr.io/techcorp/servei-cataleg:1.4.2

docker ps                          # contenidors en execució: id, imatge, estat (healthy/unhealthy), ports
docker logs -f cataleg             # stdout/stderr del procés (pino escriu JSON a stdout: per això funciona sense fitxers de log)
docker exec -it cataleg sh         # una shell dins del contenidor (Alpine: sh, no bash) per inspeccionar
docker exec cataleg node scripts/llavor.js     # executar una ordre puntual amb el mateix entorn del contenidor
docker stop cataleg                # SIGTERM i, si en 10 s no ha acabat, SIGKILL (apartat 7)
docker images                      # imatges locals i la seva mida

host.docker.internal és el nom amb què un contenidor arriba a l'amfitrió (Docker Desktop; a Linux, --add-host=host.docker.internal:host-gateway). Només té sentit en aquest experiment aïllat: a Compose i a Kubernetes els serveis es troben pel nom dins de la mateixa xarxa.

  1. Senyals, PID 1 i aturada ordenada dins del contenidor

A 04-02 vam escriure l'aturada ordenada: en rebre SIGTERM, /health/ready passa a 503, servidor.close() acaba les peticions en curs, es tanca Mongo i el procés surt, amb un límit de 10 s. Dins d'un contenidor hi ha tres detalls que poden anul·lar aquesta feina:

  1. Qui és PID 1. El procés arrencat per CMD és el PID 1 del contenidor i és l'únic que rep el senyal de docker stop o del kubelet. Amb la forma exec CMD ["node", "src/servidor.js"], PID 1 és Node i el nostre gestor s'executa. Amb la forma shell CMD node src/servidor.js, PID 1 és /bin/sh, que rep SIGTERM i no el reenvia a Node: el servei mor 10 s després per SIGKILL, amb les peticions a mitges. I amb CMD ["npm", "start"], PID 1 és npm, que tampoc no reenvia senyals de manera fiable. Per això el Dockerfile crida node directament.
  2. PID 1 no té gestors per defecte. El nucli tracta el PID 1 de manera especial: si no instal·la un gestor per a un senyal, l'ignora. Node sí que instal·la el nostre (process.on('SIGTERM')), així que estem coberts; un servei que se n'oblidés no moriria mai amb docker stop, només amb SIGKILL.
  3. Processos zombi. PID 1 ha de "recollir" els processos fills acabats. Node no llança fills als nostres serveis, però si algun dia un executa child_process, convé un init mínim: docker run --init (Docker injecta tini) o, a Kubernetes, shareProcessNamespace o tini a la imatge. És una línia barata que evita un problema difícil de diagnosticar.

Temps: docker stop espera 10 s per defecte (-t ho canvia); Compose fa servir stop_grace_period; Kubernetes, terminationGracePeriodSeconds (30 s per defecte, 05-02). El límit intern de 10 s de 04-02 està pensat per cabre dins de qualsevol d'ells.

  1. Docker Compose: l'entorn local complet de TechCorp

Docker Compose descriu en un YAML un conjunt de contenidors, xarxes i volums i els aixeca amb una sola ordre. És l'eina de l'entorn local i de l'E2E (04-01, 04-05); no és un orquestrador de producció. El fitxer viu al repositori techcorp/plataforma, a local/compose.yaml, i assumeix que els repositoris de serveis estan clonats com a directoris germans (../../servei-cataleg, etc.).

# techcorp/plataforma/local/compose.yaml
name: techcorp

services:
  # ---------- Dependències ----------
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: svc_comandes
      POSTGRES_PASSWORD: dev-comandes         # només desenvolupament local; en producció, Secret (05-02)
      POSTGRES_DB: comandes
    volumes:
      - pg-dades:/var/lib/postgresql/data      # les dades sobreviuen a docker compose down (sense -v)
    ports:
      - "5432:5432"                            # publicat només per poder connectar psql/DBeaver des del portàtil
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U svc_comandes -d comandes"]
      interval: 5s
      timeout: 3s
      retries: 10

  mongo:
    image: mongo:7
    volumes:
      - mongo-dades:/data/db
    ports:
      - "27017:27017"
    healthcheck:
      test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
      interval: 5s
      timeout: 3s
      retries: 10

  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"                            # AMQP (els serveis)
      - "15672:15672"                          # consola web http://localhost:15672 (guest/guest)
    volumes:
      - rabbitmq-dades:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
      interval: 5s
      timeout: 5s
      retries: 12

  # ---------- Tasques d'un sol ús ----------
  cataleg-llavor:                              # scripts/llavor.js de 04-02, amb la mateixa imatge del servei
    image: ghcr.io/techcorp/servei-cataleg:local
    build:
      context: ../../servei-cataleg
      secrets: [npmrc]
    command: ["node", "scripts/llavor.js"]
    environment:
      MONGO_URL: mongodb://mongo:27017
      MONGO_BD: cataleg
    depends_on:
      mongo: { condition: service_healthy }
    restart: "no"                              # acaba i no es reinicia; és idempotent (upsert)

  comandes-migracions:                         # scripts/migrar.js de 04-04, abans d'arrencar servei-comandes
    image: ghcr.io/techcorp/servei-comandes:local
    build:
      context: ../../servei-comandes
      secrets: [npmrc]
    command: ["node", "scripts/migrar.js"]
    environment:
      COMANDES_DB_URL: postgres://svc_comandes:dev-comandes@postgres:5432/comandes
    depends_on:
      postgres: { condition: service_healthy }
    restart: "no"

  # ---------- Serveis ----------
  servei-cataleg:
    image: ghcr.io/techcorp/servei-cataleg:local
    build:
      context: ../../servei-cataleg
      secrets: [npmrc]
    environment:
      PORT: "3001"
      MONGO_URL: mongodb://mongo:27017
      MONGO_BD: cataleg
      LOG_NIVELL: debug
    depends_on:
      mongo: { condition: service_healthy }
      cataleg-llavor: { condition: service_completed_successfully }
    stop_grace_period: 15s                     # marge per a l'aturada ordenada (10 s interns + folgança)

  servei-comandes:
    image: ghcr.io/techcorp/servei-comandes:local
    build:
      context: ../../servei-comandes
      secrets: [npmrc]
    environment:                               # exactament la columna "desenvolupament" de la taula de 04-03, amb noms de xarxa de Compose
      PORT: "3002"
      NODE_ENV: production
      LOG_NIVELL: debug
      COMANDES_DB_URL: postgres://svc_comandes:dev-comandes@postgres:5432/comandes
      RABBITMQ_URL: amqp://rabbitmq:5672
      CATALEG_URL: http://servei-cataleg:3001
      CLIENTS_URL: http://servei-clients:3004
      TIMEOUT_HTTP_MS: "2000"
      OUTBOX_INTERVAL_MS: "500"
      CATALEG_REMOT: "true"
    depends_on:
      postgres: { condition: service_healthy }
      rabbitmq: { condition: service_healthy }
      comandes-migracions: { condition: service_completed_successfully }
      servei-cataleg: { condition: service_started }
    stop_grace_period: 15s

  servei-clients:                              # encara no extret: l'stub de 04-04 (scripts/stubClients.js) respon c-1024
    image: ghcr.io/techcorp/servei-comandes:local
    command: ["node", "scripts/stubClients.js"]
    environment:
      PORT: "3004"

  gateway:                                     # el gateway Express de 03-04, amb les rutes /api/v1/*
    image: ghcr.io/techcorp/gateway:local
    build:
      context: ../../gateway
      secrets: [npmrc]
    ports:
      - "8080:8080"                            # l'ÚNICA porta pública del sistema
    environment:
      PORT: "8080"
      CATALEG_URL: http://servei-cataleg:3001
      COMANDES_URL: http://servei-comandes:3002
      CLIENTS_URL: http://servei-clients:3004
      MONOLIT_URL: http://host.docker.internal:3000    # el monòlit continua al portàtil, si cal
    depends_on:
      - servei-cataleg
      - servei-comandes
      - servei-clients

volumes:
  pg-dades:
  mongo-dades:
  rabbitmq-dades:

secrets:
  npmrc:
    file: ${HOME}/.npmrc                       # token de GitHub Packages per a npm ci; mai no entra a la imatge

Punts que convé entendre bé:

  • Xarxa per defecte. Compose crea una xarxa techcorp_default i hi connecta tots els serveis; cadascun resol els altres pel nom del servei. Per això CATALEG_URL=http://servei-cataleg:3001 i RABBITMQ_URL=amqp://rabbitmq:5672 són idèntics als que farem servir a Kubernetes: la configuració de 04-03 no canvia entre local i clúster.
  • ports només on cal. El gateway publica el 8080; les bases de dades i RabbitMQ es publiquen per comoditat de desenvolupament. Els serveis servei-* no publiquen ports: només s'hi arriba pel gateway o des d'altres contenidors, com en producció.
  • healthcheck + depends_on: condition. Sense condició, depends_on només ordena l'arrencada, no espera que PostgreSQL accepti connexions; servei-comandes arrencaria, config.js validaria, el pool fallaria i el /health/ready estaria en 503 fins que es connectés. Amb service_healthy Compose espera el healthcheck; amb service_completed_successfully espera que la tasca d'un sol ús acabi amb codi 0. Així les migracions sempre estan aplicades abans que arrenqui Comandes, sense scripts d'espera.
  • Tasques d'un sol ús amb la mateixa imatge. cataleg-llavor i comandes-migracions no necessiten cap altra imatge: fan servir la del servei i canvien command. És la raó per la qual el Dockerfile copia scripts/. A Kubernetes seran Job (05-02).
  • image + build. Amb tots dos, docker compose build construeix i etiqueta la imatge com a :local; docker compose up la fa servir. Si demà es vol provar la imatge que va publicar CI, n'hi ha prou de canviar :local per :sha-9f3c2ab i ometre el build.
  • servei-clients com a stub. Reutilitza scripts/stubClients.js de 04-04 des de la imatge de Comandes. Quan el servei real existeixi (ordre d'extracció de 02-02), se substitueix pel seu propi build, amb CLIENTS_DB_URL i la resta sense tocar.

  1. Operar l'entorn: up, down, logs, ps, migracions i la prova E2E

cd plataforma/local

docker compose build                    # construeix les imatges :local (fa servir la memòria cau de capes de l'apartat 4)
docker compose up -d                    # ho aixeca tot en ordre: dependències → llavor/migracions → serveis → gateway
docker compose ps                       # estat de cada servei: running (healthy), exited (0) per a les tasques d'un sol ús
docker compose logs -f servei-comandes  # logs d'un servei; sense nom, de tots, intercalats i amb prefix
docker compose exec servei-comandes sh  # shell dins d'un servei
docker compose run --rm comandes-migracions   # tornar a llançar les migracions a mà (p. ex. després d'afegir 005-*.sql)
docker compose restart servei-comandes  # reiniciar-ne un (recarrega variables si s'ha canviat el YAML després d'un 'up')
docker compose down                     # atura i esborra contenidors i xarxa; els volums es conserven
docker compose down -v                  # ...i esborra també els volums: base de dades neta

Comprovació manual del flux de comanda de tot el curs, aquesta vegada a través del gateway i amb tots els serveis en contenidors:

curl -s http://localhost:8080/api/v1/productes?ids=p-501,p-777 | jq .dades[].nom
curl -s -X POST http://localhost:8080/api/v1/comandes \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: 7c1e0b3a-e2e-0001' \
  -d '{"clientId":"c-1024","linies":[{"producteId":"p-501","quantitat":1},{"producteId":"p-777","quantitat":2}]}'
# → 202 Accepted, {"id":"com-...","estat":"PENDENT"} ; a la consola de RabbitMQ (15672) apareix comanda.creada a techcorp.esdeveniments

Com que Inventari, Pagaments i Notificacions encara no estan extrets, en local se'n simulen les respostes publicant els esdeveniments amb scripts/publicarEsdeveniment.js de 04-04, o afegint al Compose stubs consumidors (exercici 2). La prova E2E de 04-05 s'executa sobre aquest mateix entorn des del repositori plataforma:

docker compose up -d --wait             # --wait: no retorna el control fins que tots els healthchecks estan en verd
GATEWAY_URL=http://localhost:8080 npm run test:e2e   # proves/e2e/crearComanda.e2e.test.js: POST /api/v1/comandes i polling fins a CONFIRMADA (15 s)
docker compose down -v

Aquest trio d'ordres és literalment el que executarà el pipeline de 05-03 abans de promocionar a staging: si una cua està mal enllaçada o falta una variable, falla aquí i no en producció.

  1. Mida d'imatge i seguretat bàsica

Pràctica Efecte Estat a TechCorp
Base node:20-alpine (o node:20-slim si un mòdul natiu no compila amb musl) Imatge final d'uns 130 MB en lloc d'uns 450 MB; menys superfície d'atac Alpine per defecte a la plantilla
Multi-stage + npm ci --omit=dev Sense Jest, Pact ni Testcontainers en producció Sí
.dockerignore Context petit, sense .env ni .git Sí
Usuari no root (USER node) Una fallada del servei no dona root al contenidor Sí; Kubernetes ho exigirà amb runAsNonRoot (05-02)
Sense secrets a la imatge Ni .env, ni .npmrc, ni ARG amb contrasenyes (els ARG queden a l'historial de la imatge) --mount=type=secret per al token d'npm
Fixar la base per digest (node:20-alpine@sha256:...) Builds reproduïbles encara que l'etiqueta es mogui Ho gestiona el pipeline amb Renovate/Dependabot (05-03)
Escaneig de vulnerabilitats (Trivy, Grype) Detectar CVE a la base i a node_modules Etapa del pipeline; detall a 07-04
docker history / dive Veure quina capa pesa i per què Eina de diagnòstic

La regla resumida: la imatge conté codi i dependències; res més. Configuració i secrets arriben de l'entorn (04-03), les dades viuen en volums o serveis externs, i el procés no és root.

Errors Comuns i Consells

  • COPY . . al començament del Dockerfile. Cada canvi de codi repeteix npm ci. Primer package*.json, després instal·lar, després el codi.
  • Oblidar el .dockerignore. El node_modules del portàtil (amb binaris de macOS) i el .env amb la contrasenya de PostgreSQL acaben dins de la imatge publicada a ghcr.io.
  • CMD npm start o forma shell. SIGTERM no arriba a Node; cada desplegament talla peticions. Forma exec i node directament.
  • latest en qualsevol lloc que no sigui el portàtil. Sense traçabilitat ni rollback.
  • depends_on sense condition. "Funciona" al portàtil ràpid i falla al runner de CI, on PostgreSQL triga 8 s a acceptar connexions. healthcheck a cada dependència i service_healthy.
  • Publicar ports dels serveis interns "per provar". Es proven a través del gateway (o amb docker compose exec); publicar el 3002 acostuma a saltar-se l'única porta que existirà en producció.
  • Consell: executa docker compose config per veure el YAML final amb les variables substituïdes; i docker compose up --build --wait com a ordre única en començar el dia.

Exercicis

Exercici 1. Escriu el Dockerfile de producció de servei-comandes (04-04) partint del de Catàleg. Indica quines línies canvien i per què, tenint en compte que Comandes necessita migracions/ i scripts/migrar.js dins de la imatge, escolta al 3002 i el seu HEALTHCHECK no s'ha de fer servir a Kubernetes.

Exercici 2. L'equip de Comandes vol que l'E2E passi en local sense Inventari ni Pagaments reals. Afegeix al compose.yaml un servei saga-simulada que executi un script Node (scripts/sagaSimulada.js, ja escrit: consumeix comanda.creada d'una cua pròpia i publica estoc.reservat i pagament.confirmat) fent servir la imatge de Comandes. Quines variables necessita, de què depèn i per què no ha de publicar ports?

Exercici 3. Un company executa docker stop servei-cataleg i observa que triga exactament 10 s i que als logs no apareix "aturant". El seu Dockerfile acaba amb CMD npm start. Explica la causa, la correcció, i quin altre símptoma tindria a Kubernetes amb terminationGracePeriodSeconds: 30.

Solucions

Solució 1. Canvien: COPY --chown=node:node migracions ./migracions afegit al costat de src i scripts (el Job de 05-02 i comandes-migracions de Compose executen node scripts/migrar.js des d'aquesta imatge i llegeixen migracions/*.sql); EXPOSE 3002; el HEALTHCHECK apunta a http://localhost:3002/health/live. Com que Kubernetes ignora HEALTHCHECK (fa servir livenessProbe), es pot deixar per a Compose o eliminar-lo; l'important és no confondre'l amb la probe. Tota la resta (dues etapes, npm ci --omit=dev amb el secret npmrc, --chown=node:node, USER node, CMD ["node","src/servidor.js"]) és idèntica: és el que la plantilla de 04-01 dona fet.

Solució 2.

  saga-simulada:
    image: ghcr.io/techcorp/servei-comandes:local
    command: ["node", "scripts/sagaSimulada.js"]
    environment:
      RABBITMQ_URL: amqp://rabbitmq:5672
      LOG_NIVELL: debug
    depends_on:
      rabbitmq: { condition: service_healthy }
      servei-comandes: { condition: service_started }   # perquè la topologia (exchange techcorp.esdeveniments) ja estigui declarada
    restart: unless-stopped

Només necessita RABBITMQ_URL (parla únicament per esdeveniments, com Inventari i Pagaments a 03-04: "no exposats"). No publica ports perquè no exposa HTTP i perquè res de fora de la xarxa de Compose no hi ha de parlar; si un dia ho fes, seria pel gateway. Depèn que RabbitMQ estigui sa i que Comandes hagi arrencat (declara la topologia en connectar; alternativament l'script la pot declarar ell mateix amb missatgeria/topologia de la llibreria, i llavors la segona dependència sobra).

Solució 3. Amb CMD npm start, PID 1 és npm, que arrenca node src/servidor.js com a fill. docker stop envia SIGTERM al PID 1 (npm), que no el reenvia a Node; el gestor process.on('SIGTERM') de 04-02 no s'executa mai, així que no hi ha línia "aturant" ni servidor.close(). Passats 10 s Docker envia SIGKILL i tot mor de cop: les peticions en curs reben connection reset. Correcció: CMD ["node", "src/servidor.js"] (forma exec, Node com a PID 1). A Kubernetes el símptoma seria que cada rolling update (05-04) triga 30 s per pod en lloc d'1-2 s i que, durant aquests 30 s, el pod continua rebent trànsit fins que el readinessProbe falla, perquè /health/ready no va passar mai a 503; amb la forma exec, el mateix servei es marca com a no llest a l'instant del SIGTERM.

Conclusió

Hem convertit els processos Node dels mòduls anteriors en unitats desplegables: una imatge per servei, construïda amb un Dockerfile multi-stage sobre node:20-alpine (npm ci --omit=dev amb el token d'npm com a secret de build, COPY --chown=node:node, USER node, EXPOSE, HEALTHCHECK opcional a /health/live, CMD ["node","src/servidor.js"] en forma exec perquè SIGTERM arribi a l'aturada ordenada de 04-02), amb .dockerignore i un ordre d'instruccions que aprofita la memòria cau de capes, etiquetada amb versió semàntica i sha-<commit> a ghcr.io/techcorp/ i mai amb latest. I hem aixecat per primera vegada el sistema complet amb Docker Compose (plataforma/local/compose.yaml): PostgreSQL 16, MongoDB 7 i RabbitMQ amb healthchecks, tasques d'un sol ús per a la llavor i les migracions, servei-cataleg, servei-comandes, l'stub de Clients i el gateway al 8080, amb les mateixes variables d'entorn de 04-03 i els mateixos noms DNS de 03-05, i a sobre l'única prova E2E del curs. Compose és perfecte per a un portàtil, però no reinicia un contenidor caigut en una altra màquina, no reparteix rèpliques ni gestiona desplegaments sense talls: per a això hi ha Kubernetes, que a la lliçó següent executarà aquestes mateixes imatges amb els ConfigMap i Secret que 04-03 va deixar anomenats.

Curs de Microserveis

Mòdul 1: Introducció als Microserveis

Mòdul 2: Disseny de Microserveis

Mòdul 3: Comunicació entre Microserveis

Mòdul 4: Implementació de Microserveis

Mòdul 5: Desplegament i Orquestració

Mòdul 6: Monitoratge i Manteniment

Mòdul 7: Seguretat en Microserveis

Mòdul 8: Casos d'Estudi i Exemples Pràctics

© Copyright 2026. Tots els drets reservats