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
- Per què contenidors per a microserveis
- Conceptes: imatge, capa, contenidor, registre, volum i xarxa
- El
Dockerfilede producció deservei-cataleg, línia a línia .dockerignore, memòria cau de capes i ordre de les instruccions- Etiquetatge d'imatges i registre
- Ordres bàsiques de Docker
- Senyals, PID 1 i aturada ordenada dins del contenidor
- Docker Compose: l'entorn local complet de TechCorp
- Operar l'entorn:
up,down,logs,ps, migracions i la prova E2E - Mida d'imatge i seguretat bàsica
- 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 fernpm 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.
- 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.
- El
Dockerfile de producció de servei-cataleg, línia a línia
Dockerfile de producció de servei-cataleg, línia a líniaEl 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 denode:20).AS dependenciesdona nom a l'etapa per poder copiar-ne coses després.WORKDIR /app: crea/appi 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 ciinstal·la exactament el que diu elpackage-lock.json(reproduïble; falla si el lock no quadra ambpackage.json);--omit=devdeixa foranodemon,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; elconfig.jsde 04-03 el llegeix com una variable més.COPY --from=dependencies --chown=node:node /app/node_modules ./node_modules: porta nomésnode_modulesja instal·lat.--chown=node:nodeassigna la propietat a l'usuarinode(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
COPYseparats per apackage.json,srciscripts:scripts/hi entra perquèllavor.jss'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òsCMD) 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-po a Compose/Kubernetes). Coincideix ambPORT=3001de 04-02.HEALTHCHECK: Docker executawgetcontra/health/livecada 30 s, amb 10 s de gràcia en arrencar; tres fallades seguides marquen el contenidorunhealthy. Alpine portawgeta BusyBox, així que no cal instal·larcurl. Kubernetes ignoraHEALTHCHECKi fa servir les seves pròpies probes (05-02); a Compose sí que és útil per adepends_on(apartat 8).CMD ["node", "src/servidor.js"]: l'ordre d'arrencada, igual que l'scriptstartde 04-02, però sense passar pernpm(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.
.dockerignore, memòria cau de capes i ordre de les instruccions
.dockerignore, memòria cau de capes i ordre de les instruccionsCOPY 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:
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.
- 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:
latestno 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 mateixDeploymentpoden executar versions diferents segons quan van fer pull, i unrollbacké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
- 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 midahost.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.
- 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:
- Qui és PID 1. El procés arrencat per
CMDés el PID 1 del contenidor i és l'únic que rep el senyal dedocker stopo del kubelet. Amb la forma execCMD ["node", "src/servidor.js"], PID 1 és Node i el nostre gestor s'executa. Amb la forma shellCMD 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 ambCMD ["npm", "start"], PID 1 ésnpm, que tampoc no reenvia senyals de manera fiable. Per això elDockerfilecridanodedirectament. - 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 ambdocker stop, només amb SIGKILL. - 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é uninitmínim:docker run --init(Docker injectatini) o, a Kubernetes,shareProcessNamespaceotinia 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.
- 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 imatgePunts que convé entendre bé:
- Xarxa per defecte. Compose crea una xarxa
techcorp_defaulti hi connecta tots els serveis; cadascun resol els altres pel nom del servei. Per aixòCATALEG_URL=http://servei-cataleg:3001iRABBITMQ_URL=amqp://rabbitmq:5672són idèntics als que farem servir a Kubernetes: la configuració de 04-03 no canvia entre local i clúster. portsnomés on cal. El gateway publica el 8080; les bases de dades i RabbitMQ es publiquen per comoditat de desenvolupament. Els serveisservei-*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_onnomés ordena l'arrencada, no espera que PostgreSQL accepti connexions;servei-comandesarrencaria,config.jsvalidaria, el pool fallaria i el/health/readyestaria en 503 fins que es connectés. Ambservice_healthyCompose espera el healthcheck; ambservice_completed_successfullyespera 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-llavoricomandes-migracionsno necessiten cap altra imatge: fan servir la del servei i canviencommand. És la raó per la qual elDockerfilecopiascripts/. A Kubernetes seranJob(05-02). image+build. Amb tots dos,docker compose buildconstrueix i etiqueta la imatge com a:local;docker compose upla fa servir. Si demà es vol provar la imatge que va publicar CI, n'hi ha prou de canviar:localper:sha-9f3c2abi ometre elbuild.servei-clientscom a stub. Reutilitzascripts/stubClients.jsde 04-04 des de la imatge de Comandes. Quan el servei real existeixi (ordre d'extracció de 02-02), se substitueix pel seu propibuild, ambCLIENTS_DB_URLi la resta sense tocar.
- Operar l'entorn:
up, down, logs, ps, migracions i la prova E2E
up, down, logs, ps, migracions i la prova E2Ecd 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 netaComprovació 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.esdevenimentsCom 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 -vAquest 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ó.
- 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 delDockerfile. Cada canvi de codi repeteixnpm ci. Primerpackage*.json, després instal·lar, després el codi.- Oblidar el
.dockerignore. Elnode_modulesdel portàtil (amb binaris de macOS) i el.envamb la contrasenya de PostgreSQL acaben dins de la imatge publicada aghcr.io. CMD npm starto forma shell. SIGTERM no arriba a Node; cada desplegament talla peticions. Forma exec inodedirectament.latesten qualsevol lloc que no sigui el portàtil. Sense traçabilitat ni rollback.depends_onsensecondition. "Funciona" al portàtil ràpid i falla al runner de CI, on PostgreSQL triga 8 s a acceptar connexions.healthchecka cada dependència iservice_healthy.- Publicar ports dels serveis interns "per provar". Es proven a través del gateway (o amb
docker compose exec); publicar el3002acostuma a saltar-se l'única porta que existirà en producció. - Consell: executa
docker compose configper veure el YAML final amb les variables substituïdes; idocker compose up --build --waitcom 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-stoppedNomé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
- Conceptes Bàsics de Microserveis
- Avantatges i Desavantatges dels Microserveis
- Comparació amb l'Arquitectura Monolítica
- Quan Adoptar Microserveis: Criteris de Decisió
- El Cas Pràctic del Curs: la Botiga Online de TechCorp
Mòdul 2: Disseny de Microserveis
- Principis de Disseny de Microserveis
- Descomposició d'Aplicacions Monolítiques
- Definició de Bounded Contexts
- Gestió de Dades: una Base de Dades per Servei
- Consistència Distribuïda: Sagues, CQRS i Event Sourcing
Mòdul 3: Comunicació entre Microserveis
- APIs RESTful
- Missatgeria Asíncrona
- Protocols de Comunicació: gRPC, GraphQL
- API Gateway i Backend for Frontend
- Descobriment de Serveis i Balanceig de Càrrega
- Contractes i Versionat d'APIs
Mòdul 4: Implementació de Microserveis
- Elecció de Tecnologies i Eines
- Desenvolupament d'un Microservei Simple
- Gestió de Configuració
- Integració Pràctica: Consumir APIs i Publicar Esdeveniments
- Proves en Microserveis: Unitàries, d'Integració i de Contracte
Mòdul 5: Desplegament i Orquestració
- Contenidors i Docker
- Orquestració amb Kubernetes
- CI/CD per a Microserveis
- Estratègies de Desplegament: Rolling, Blue-Green i Canary
- Service Mesh: Istio i Linkerd
Mòdul 6: Monitoratge i Manteniment
- Monitoratge i Logging
- Traçabilitat Distribuïda amb OpenTelemetry
- Gestió d'Errors i Recuperació
- Escalabilitat i Rendiment
- SLOs, Alertes i Gestió d'Incidents
Mòdul 7: Seguretat en Microserveis
- Autenticació i Autorització
- Seguretat en la Comunicació
- Pràctiques de Seguretat
- Seguretat en Contenidors i Kubernetes
