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ó
.dockerignore Context petit, sense .env ni .git
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