El Dockerfile amb què vas tancar la lliçó anterior funciona: construeix auroralibros/aurora-api:1.0.0, arrenca Express en un contenidor i respon a curl. Però li falten les peces que separen una imatge que funciona d'una imatge que pots posar en producció sense envermellir. Ara mateix, l'API s'executa com a root dins del contenidor, ningú no sap qui la va construir ni des de quin commit, Docker no té manera de saber si el procés és viu però encallat, la versió de Node està escrita a foc al FROM, i no pots executar la imatge amb una comanda diferent sense perdre completament el seu comportament per defecte. Aquesta lliçó resol les cinc coses. Aprendràs ARG i la seva diferència crítica amb ENV, el binomi ENTRYPOINT + CMD amb les seves quatre combinacions, USER per deixar de ser root, LABEL amb l'estàndard OCI, HEALTHCHECK contra l'endpoint /salut que vas preparar a la lliçó 01-07, i les tres instruccions menors —VOLUME, STOPSIGNAL, ONBUILD i SHELL— que convé conèixer encara que les facis servir poc. Al final tindràs el Dockerfile professional d'aurora-api.
Contingut
ARGdavant d'ENV: àmbit i momentARGabans del primerFROMENTRYPOINTdavant deCMD: les quatre combinacions- El patró
docker-entrypoint.shi per què importaexec "$@" USER: deixar d'executar com a rootLABELi les etiquetes estàndard OCIHEALTHCHECK: que Docker vigili el teu serveiVOLUMEi per què declarar-lo sol ser mala ideaSTOPSIGNALi la gestió de senyalsONBUILDiSHELL- El Dockerfile professional d'
aurora-api
ARG davant d'ENV: àmbit i moment
ARG davant d'ENV: àmbit i momentTotes dues defineixen variables. La confusió entre elles és constant, i la diferència és senzilla d'enunciar: ARG existeix només durant la construcció; ENV existeix també dins del contenidor en execució.
| Aspecte | ARG |
ENV |
|---|---|---|
| Moment en què existeix | Només durant docker build |
Durant la build i en execució |
| Visible dins del contenidor | No | Sí (printenv, process.env) |
| Es defineix des de fora amb | --build-arg CLAU=valor |
-e CLAU=valor a docker run |
| Valor per defecte | ARG CLAU=valor |
ENV CLAU=valor |
| Sobreescrivible en executar | No existeix en execució | Sí, amb -e |
Apareix a docker image history? |
Sí, el seu valor | Sí, el seu valor |
| Ús típic | Versions, rutes de build, metadades de CI | Configuració per defecte de l'aplicació |
Un exemple que ho demostra tot. Crea /tmp/arg-env/Dockerfile:
# syntax=docker/dockerfile:1
FROM alpine:3.21
# Argument de construcció, amb valor per defecte
ARG SALUTACIO_BUILD=hola-des-de-la-build
# Variable d'entorn, amb valor per defecte
ENV SALUTACIO_RUN=hola-des-de-lexecucio
# Durant la build, TOTES DUES estan disponibles
RUN echo "BUILD veu ARG: $SALUTACIO_BUILD" && echo "BUILD veu ENV: $SALUTACIO_RUN"
CMD ["sh", "-c", "echo \"RUN veu ARG: [$SALUTACIO_BUILD]\"; echo \"RUN veu ENV: [$SALUTACIO_RUN]\""]Durant la construcció, totes dues estan disponibles. Ara executa:
L'ARG està buit. No és que valgui una altra cosa: no existeix. Es va evaporar quan va acabar la build. L'ENV continua sent-hi.
I així es passa un valor des de fora:
docker build --build-arg SALUTACIO_BUILD=valor-injectat --progress=plain --no-cache -t argenv . 2>&1 | grep "BUILD veu ARG"El pont entre totes dues
El patró més útil és combinar-les: un ARG que alimenta un ENV.
Així, --build-arg VERSIO_APP=1.2.3 a la construcció acaba sent una variable d'entorn consultable dins del contenidor. És com s'injecten números de versió i hashes de commit des d'un pipeline de CI (lliçó 06-02).
L'advertència important: ARG no és un secret
Això s'ha de gravar a foc:
docker build --build-arg DB_PASSWORD=superSecreta2026 -t filtrada .
docker image history filtrada --no-trunc | head -5CREATED BY
|1 DB_PASSWORD=superSecreta2026 /bin/sh -c echo "connectant amb $DB_PASSWORD" > /app/config.txtAquí està la contrasenya, en clar, a l'historial de la imatge. Qualsevol que descarregui la imatge la pot llegir amb una sola comanda, sense arrencar res. I publicant al repositori públic auroralibros/aurora-api, a Internet.
És la mateixa regla que arrossegues des de la lliçó 01-07, ara amb una via de fuita addicional: no només ENV filtra secrets, també ARG. Per passar credencials a una build de manera segura existeixen els muntatges de secrets de BuildKit (RUN --mount=type=secret), que no deixen rastre en cap capa; s'estudien a la lliçó 05-05. Per a Aurora Libros, la regla es manté intacta: les credencials no entren a la imatge ni en construir ni en executar; s'injecten en arrencar el contenidor.
ARG abans del primer FROM
ARG abans del primer FROMARG té una peculiaritat d'àmbit que sorprèn tothom: un ARG declarat abans del primer FROM només és visible a les línies FROM, no dins de la imatge.
# syntax=docker/dockerfile:1
# ARG "global": viu fora de qualsevol etapa. Només el veuen els FROM.
ARG NODE_VERSION=22
ARG ALPINE_VERSION=3.21
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION}
# Aquí dins, NODE_VERSION ja no existeix llevat que es torni a declarar
ARG NODE_VERSION
RUN echo "Construint sobre Node ${NODE_VERSION}"Aquesta doble declaració (ARG NODE_VERSION sense valor, dins de l'etapa) és la manera de "reimportar" l'argument global. Sense ella, la variable estaria buida dins del RUN.
La utilitat pràctica és enorme: parametritzar la versió base sense tocar el Dockerfile.
# Build per defecte: Node 22
docker build -t aurora-api:node22 .
# Provar l'API amb Node 23 sense editar res
docker build --build-arg NODE_VERSION=23 -t aurora-api:node23 .Casos d'ús reals per a Aurora Libros:
| Escenari | Comanda |
|---|---|
| Provar la propera versió major de Node abans d'adoptar-la | --build-arg NODE_VERSION=23 |
| Reproduir un bug a la versió antiga | --build-arg NODE_VERSION=20 |
| Matriu de compatibilitat a CI | Un job per versió, mateix Dockerfile |
Un avís: parametritzar la base és còmode, però el valor per defecte ha de ser el de producció. Si l'ARG no té valor per defecte i algú construeix sense --build-arg, obtindrà node:-alpine, que no existeix, i un error críptic de manifest.
ENTRYPOINT davant de CMD: les quatre combinacions
ENTRYPOINT davant de CMD: les quatre combinacionsCMD, que ja coneixes, defineix què s'executa en arrencar el contenidor i es descarta completament si passes una comanda a docker run. ENTRYPOINT defineix un executable que no es descarta: els arguments de docker run se li afegeixen darrere.
La combinació de totes dues produeix quatre escenaris. Aquesta taula és la referència que convé tenir a mà:
| # | Dockerfile | docker run imatge executa |
docker run imatge extra executa |
|---|---|---|---|
| 1 | Només CMD ["node","server.js"] |
node server.js |
extra (el CMD es descarta) |
| 2 | Només ENTRYPOINT ["node","server.js"] |
node server.js |
node server.js extra |
| 3 | ENTRYPOINT ["node"] + CMD ["server.js"] |
node server.js |
node extra (el CMD se substitueix) |
| 4 | ENTRYPOINT ["node","server.js"] + CMD ["--port=3000"] |
node server.js --port=3000 |
node server.js extra |
La regla que resumeix les quatre files: ENTRYPOINT és l'executable fix; CMD són els arguments per defecte, i són l'única cosa que l'usuari pot reemplaçar.
Comprova-ho amb la imatge que ja tens:
# Cas 1: només CMD (la teva imatge 1.0.0)
docker run --rm auroralibros/aurora-api:1.0.0 node --versionEl CMD ["node","server.js"] ha desaparegut: el servidor no arrenca.
# Cas 3: ENTRYPOINT + CMD
printf 'FROM auroralibros/aurora-api:1.0.0\nENTRYPOINT ["node"]\nCMD ["server.js"]\n' > /tmp/Dockerfile.ep
docker build -q -t aurora-ep -f /tmp/Dockerfile.ep /tmp
docker run --rm aurora-ep --versionAquí --version ha substituït server.js, però node continua sent-hi: s'ha executat node --version. L'executable és intocable.
El patró recomanat
Avantatges davant de només CMD:
- La imatge té una identitat clara. És "la imatge que executa node", no "una imatge genèrica".
- És autodocumentada.
docker run lasevaimatge --helpfunciona sense més. - Evita accidents. Ningú no arrenca per descuit un contenidor que no fa el que la imatge promet.
I el seu inconvenient principal: depurar costa més. Amb només CMD, un docker run --rm -it lasevaimatge sh et dona un intèrpret d'ordres. Amb ENTRYPOINT ["node"], aquella comanda intenta executar node sh i falla. Per a això hi ha --entrypoint:
--entrypoint substitueix l'executable fix. Compte amb un detall que despista: --entrypoint accepta un sol valor, i tot el que vingui després del nom de la imatge se li passa com a arguments:
Formes shell i exec, un altre cop
Tot el que has après a la lliçó 02-03 sobre CMD s'aplica igual a ENTRYPOINT, agreujat:
Amb ENTRYPOINT en forma shell passen dues coses dolentes, no una:
- El PID 1 és
shi no reenvia SIGTERM: els 10 segons d'espera i el SIGKILL de la lliçó anterior. CMDs'ignora completament, i els arguments dedocker runtambé. L'ENTRYPOINTen forma shell s'empassa tot el mecanisme d'arguments.
Amb ENTRYPOINT, la forma exec no és una recomanació: és obligatòria.
- El patró
docker-entrypoint.sh i per què importa exec "$@"
docker-entrypoint.sh i per què importa exec "$@"Quan l'arrencada necessita lògica —esperar una dependència, generar un fitxer de configuració, aplicar migracions—, l'ENTRYPOINT apunta a un script.
Crea ~/aurora-libros/api/docker-entrypoint.sh:
#!/bin/sh
# Script d'arrencada d'aurora-api
# S'executa abans del procés principal i li cedeix el control amb exec "$@"
set -e # Avorta davant del primer error: millor no arrencar que arrencar malament
echo "[entrypoint] Iniciant aurora-api en mode ${NODE_ENV:-development}"
# Validació de configuració obligatòria: fallar aviat i amb un missatge clar
if [ -z "$DB_HOST" ]; then
echo "[entrypoint] AVÍS: DB_HOST no definida; es farà servir localhost"
fi
# Espera activa que la base de dades accepti connexions.
# Evita el clàssic ECONNREFUSED quan l'API arrenca abans que PostgreSQL.
if [ -n "$DB_HOST" ] && [ "$ESPERAR_DB" = "true" ]; then
echo "[entrypoint] Esperant ${DB_HOST}:${DB_PORT:-5432}..."
intents=0
until nc -z "$DB_HOST" "${DB_PORT:-5432}" 2>/dev/null; do
intents=$((intents + 1))
if [ "$intents" -ge 30 ]; then
echo "[entrypoint] ERROR: la base de dades no respon després de 30 intents"
exit 1
fi
sleep 1
done
echo "[entrypoint] Base de dades disponible després de ${intents}s"
fi
echo "[entrypoint] Cedint el control a: $@"
# LA LÍNIA CLAU
exec "$@"I el Dockerfile l'incorpora així:
COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "server.js"]El flux complet:
sequenceDiagram
participant D as docker run
participant E as docker-entrypoint.sh (PID 1)
participant N as node server.js
D->>E: Arrenca ENTRYPOINT amb CMD com a arguments ($@)
E->>E: set -e, comprova variables
E->>E: Espera que DB_HOST respongui
E->>N: exec "$@" · REEMPLAÇA el procés
Note over E,N: node hereta el PID 1;<br/>l'script deixa d'existir
D->>N: docker stop → SIGTERM arriba DIRECTAMENT a node
N->>N: Tancament net en 0,3 s
Per què exec "$@" i no simplement "$@"
És la part que s'ha d'entendre de debò.
"$@"són tots els arguments rebuts, cadascun entre cometes per separat. Com que l'ENTRYPOINTés l'script i elCMDés["node","server.js"],"$@"valnode server.js. Les cometes són imprescindibles: sense elles, un argument amb espais es partiria en dos.execés l'essencial. Senseexec, l'intèrpret d'ordres llança un fill i es queda esperant: el PID 1 continua sent l'script, i estàs exactament a l'escenari de la forma shell de la lliçó 02-03 —SIGTERM a l'script, que no el reenvia, deu segons i SIGKILL—. Ambexec, l'intèrpret es reemplaça a si mateix pernode: mateix PID, mateixos descriptors de fitxer, l'script desapareix de l'arbre de processos inodepassa a ser el PID 1.
Comprova-ho amb l'script en marxa:
El PID 1 és node, no sh. L'script es va executar, va fer la seva feina i es va apartar. I per tant:
Si en traguessis l'exec, aquesta xifra seria de 10 segons. Un caràcter de diferència, quatre lletres, i tot el comportament d'aturada del contenidor canvia.
Nota pràctica: l'script fa servir nc (netcat), que a node:22-alpine no ve instal·lat. Caldria afegir-hi RUN apk add --no-cache netcat-openbsd. A la versió final d'aquesta lliçó s'opta per no incloure l'espera activa, perquè Docker Compose resol les dependències d'arrencada de manera més neta amb depends_on i condicions de salut (lliçó 04-02). El patró, però, convé conèixer-lo: te'l trobaràs a les imatges oficials de PostgreSQL i MySQL, que el fan servir justament així.
USER: deixar d'executar com a root
USER: deixar d'executar com a rootAra mateix la teva API s'executa com a root dins del contenidor. Comprova-ho:
uid=0 és root. Un contenidor està aïllat, però aquell aïllament no és una muralla infranquejable: si apareix una vulnerabilitat d'escapament del runtime o muntes malament un volum, el root del contenidor pot convertir-se en root de l'amfitrió. Executar com a usuari sense privilegis elimina tota aquesta classe de problemes per un cost de tres línies.
La instrucció és USER, i afecta totes les instruccions posteriors del Dockerfile i el procés del contenidor:
Crear l'usuari a Alpine
node:22-alpine ja porta un usuari node (uid 1000), així que n'hi hauria prou amb USER node. Però convé saber crear-lo, perquè en altres bases no existeix:
Opcions de les eines d'Alpine (BusyBox), diferents de les de Debian:
| Opció | Significat |
|---|---|
-g 1001 / -u 1001 |
GID i UID explícits. Fixar-los importa perquè els permisos de volums muntats coincideixin |
-S |
System: compte de sistema, sense contrasenya ni caducitat |
-G aurora |
Grup primari de l'usuari |
-D (a adduser) |
Sense contrasenya |
A Debian/Ubuntu l'equivalent seria groupadd -g 1001 aurora && useradd -u 1001 -g aurora -m -s /bin/sh aurora.
L'ordre correcte
Aquest és el punt on falla tothom:
# ❌ MALAMENT: canviar a USER abans d'instal·lar
USER node
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev # EACCES: permission denied# ✅ BÉ: instal·lar com a root, canviar d'usuari al final
WORKDIR /app
COPY --chown=node:node package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]La seqüència correcta és: instal·lar i copiar com a root (que pot escriure on vulgui), assignar la propietat en la mateixa operació de còpia amb --chown, i posar USER just abans del CMD. Com vas veure a 02-03, --chown al COPY evita un RUN chown -R posterior que duplicaria la mida de la capa pel copy-on-write.
Comprova el resultat a la imatge final:
La conseqüència immediata: ports privilegiats
Un usuari sense privilegis no pot escoltar en ports per sota del 1024. Si la teva aplicació fes servir el port 80, deixaria d'arrencar:
Per això aurora-api escolta al 3000, i per això el contenidor de Nginx del mòdul 4 requerirà cura. La solució mai no és tornar a root: és escoltar en un port alt i publicar-lo on calgui amb -p 80:3000, ja que el mapatge el fa Docker a l'amfitrió, no el procés.
L'enduriment complet de la seguretat en execució —capabilities, --read-only, no-new-privileges, perfils seccomp, user namespaces— és la lliçó 05-03. Aquí et quedes amb l'essencial: un contenidor de producció no s'executa com a root.
LABEL i les etiquetes estàndard OCI
LABEL i les etiquetes estàndard OCILABEL afegeix metadades arbitràries a la imatge en forma de parells clau-valor. No canvia el comportament, no pesa res, i respon a preguntes que en producció són urgents: de quin commit va sortir aquesta imatge? qui la manté? de quina versió és?
Agrupa sempre diverses etiquetes en una sola instrucció: cada LABEL crea una capa de metadades.
L'estàndard OCI
Perquè les eines puguin llegir aquestes dades existeix un vocabulari estàndard, org.opencontainers.image.*, definit per l'Open Container Initiative (la mateixa de la lliçó 01-03):
| Etiqueta | Contingut |
|---|---|
org.opencontainers.image.title |
Nom llegible del component |
org.opencontainers.image.description |
Descripció breu |
org.opencontainers.image.version |
Versió del programari empaquetat |
org.opencontainers.image.authors |
Responsables, amb contacte |
org.opencontainers.image.vendor |
Organització propietària |
org.opencontainers.image.licenses |
Llicència en format SPDX |
org.opencontainers.image.source |
URL del repositori de codi |
org.opencontainers.image.documentation |
URL de la documentació |
org.opencontainers.image.revision |
Hash del commit exacte |
org.opencontainers.image.created |
Data de construcció (RFC 3339) |
org.opencontainers.image.base.name |
Imatge base utilitzada |
Aplicades a Aurora Libros, combinant ARG per al que varia a cada build:
ARG VERSION=1.1.0
ARG REVISION=desconeguda
ARG CREATED=desconeguda
LABEL org.opencontainers.image.title="aurora-api" \
org.opencontainers.image.description="API REST del catàleg d'Aurora Libros S.L." \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.authors="[email protected]" \
org.opencontainers.image.vendor="Aurora Libros S.L." \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/auroralibros/aurora-libros" \
org.opencontainers.image.documentation="https://github.com/auroralibros/aurora-libros/blob/main/README.md" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${CREATED}" \
org.opencontainers.image.base.name="docker.io/library/node:22-alpine"Els tres primers ARG s'omplen a la build, típicament des del pipeline:
docker build \
--build-arg VERSION=1.1.0 \
--build-arg REVISION="$(git rev-parse --short HEAD)" \
--build-arg CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-t auroralibros/aurora-api:1.1.0 .I així es llegeixen després:
docker image inspect auroralibros/aurora-api:1.1.0 \
--format '{{range $k, $v := .Config.Labels}}{{$k}} = {{$v}}
{{end}}'org.opencontainers.image.authors = [email protected]
org.opencontainers.image.base.name = docker.io/library/node:22-alpine
org.opencontainers.image.created = 2026-08-04T09:14:22Z
org.opencontainers.image.description = API REST del catàleg d'Aurora Libros S.L.
org.opencontainers.image.licenses = MIT
org.opencontainers.image.revision = 7a3f912
org.opencontainers.image.source = https://github.com/auroralibros/aurora-libros
org.opencontainers.image.title = aurora-api
org.opencontainers.image.vendor = Aurora Libros S.L.
org.opencontainers.image.version = 1.1.0El valor real d'això es veu a les tres de la matinada d'un dimarts: producció falla, tens un contenidor corrent i necessites saber exactament quin codi porta a dins. Un docker inspect et dona el commit 7a3f912, i amb ell el git log t'ho explica tot. Sense aquella etiqueta, comença l'arqueologia.
Les etiquetes també serveixen per filtrar, amb la sintaxi de la lliçó 01-04:
HEALTHCHECK: que Docker vigili el teu servei
HEALTHCHECK: que Docker vigili el teu serveiUn contenidor pot estar Up i ser completament inútil: el procés viu però el bucle d'esdeveniments està bloquejat, el pool de connexions esgotat o l'aplicació retornant 500 a tot. docker ps diria Up 3 hours tan tranquil. HEALTHCHECK dona a Docker una manera de comprovar la salut real.
| Opció | Per defecte | Què controla |
|---|---|---|
--interval |
30s | Cada quant s'executa la comprovació |
--timeout |
30s | Quant s'espera que respongui abans de donar-la per fallida |
--start-period |
0s | Període de gràcia inicial: les fallades d'aquí no compten |
--start-interval |
5s | Interval durant el període de gràcia (versions recents) |
--retries |
3 | Fallades consecutives necessàries per declarar unhealthy |
La comanda determina l'estat pel seu codi de sortida: 0 = sa, 1 = malalt. Qualsevol altre valor es tracta com a error.
--start-period mereix atenció especial. Sense ell, una aplicació que triga 20 segons a arrencar seria marcada unhealthy durant aquella arrencada legítima, i en un orquestrador la matarien i reiniciarien en un bucle etern. El període de gràcia diu: "durant els primers N segons, si falla, no ho comptis".
El healthcheck d'aurora-api
Aurora Libros ja té l'endpoint /salut des de la lliçó 01-07, dissenyat precisament per a això: retorna 200 si la base de dades i la memòria cau responen, i 503 si no.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000/salut || exit 1Detalls de l'elecció de la comanda:
wgeti nocurl:node:22-alpineportawget(de BusyBox) però nocurl. Fer servircurlobligaria a unapk add curlque afegeix uns 4 MB. Si prefereixescurl, l'opció éscurl -f http://localhost:3000/salut || exit 1, on-ffa que retorni codi d'error davant d'un HTTP 4xx/5xx.--spider: no descarrega el cos, només comprova que el recurs respon.--tries=1: sense reintents interns; ja reintenta Docker amb--retries.|| exit 1: normalitza qualsevol fallada al codi 1, que és el que Docker espera.localhost: la comprovació s'executa dins del contenidor, així quelocalhostés el servei mateix. No necessita ports publicats.- Els temps: cada 30 s, amb 3 s de paciència, 10 s de gràcia en arrencar i 3 fallades seguides abans de declarar-lo malalt. En el pitjor cas, un servei caigut es detecta en uns 90 segons.
Veure-ho funcionar
docker run -d --name aurora-hc -p 3000:3000 auroralibros/aurora-api:1.1.0
docker ps --filter name=aurora-hc --format "table {{.Names}}\t{{.Status}}"Acabat d'arrencar:
health: starting és el període de gràcia. Espera i torna a mirar:
unhealthy, i amb raó: /salut retorna 503 perquè no hi ha PostgreSQL ni Redis, exactament com a les lliçons anteriors. El healthcheck està fent la seva feina amb precisió total: el procés viu, però el servei no està operatiu. Justament la distinció que Up no sap fer.
L'historial complet és a les metadades:
{
"Status": "unhealthy",
"FailingStreak": 3,
"Log": [
{
"Start": "2026-08-04T09:32:11.442Z",
"End": "2026-08-04T09:32:11.503Z",
"ExitCode": 1,
"Output": ""
}
]
}FailingStreak: 3 són les tres fallades consecutives que van disparar el canvi d'estat. El camp Log desa les últimes comprovacions amb la seva sortida, que és on mires quan un healthcheck falla i no saps per què.
Els estats possibles:
| Estat | Significat |
|---|---|
starting |
Dins del --start-period; les fallades no compten |
healthy |
L'última comprovació va sortir amb codi 0 |
unhealthy |
S'han assolit --retries fallades consecutives |
I per què importa més enllà de la columna de docker ps:
- Docker Compose pot esperar que un servei estigui
healthyabans d'arrencar-ne un altre ambdepends_on: condition: service_healthy(lliçó 04-02). És la solució neta al problema que intentava resoldre a mà l'script de l'apartat 4. - Docker Swarm reemplaça automàticament les rèpliques
unhealthy(lliçó 06-03). - Kubernetes té el seu propi mecanisme equivalent, les probes (lliçó 06-05).
Neteja:
HEALTHCHECK NONE
Si la teva imatge base defineix un healthcheck que no et serveix, el pots desactivar:
És rar, però apareix en heretar d'imatges corporatives amb comprovacions que no s'apliquen al teu cas.
VOLUME i per què declarar-lo sol ser mala idea
VOLUME i per què declarar-lo sol ser mala ideaVOLUME declara que un directori de la imatge s'ha de muntar com a volum:
Quan arrenquis un contenidor d'aquella imatge sense especificar res, Docker crearà automàticament un volum anònim per a aquella ruta.
Sona útil, i tanmateix la recomanació general és no posar-lo. Quatre raons:
- Genera volums anònims orfes. Cada
docker runsense-vexplícit crea un volum amb nom aleatori que no s'esborra en eliminar el contenidor (llevat dedocker rm -v). En una màquina de desenvolupament s'acumulen desenes de gigabytes de volums amb noms comf3a9c2e1b8...que ningú no sap si són importants. Els veuràs adocker system dfa la lliçó 02-05. - No es pot desfer. Un cop una imatge declara
VOLUME /dades, qui la fa servir no ho pot treure. Li imposa una decisió d'emmagatzematge que potser no li convé. - Trenca el
COPYposterior. Tot el que escriguis en aquella ruta després delVOLUMEal Dockerfile es perd silenciosament, sense cap avís. És un error desesperant de diagnosticar. - És una decisió d'execució, no d'imatge. Qui munta què i on ho decideix qui desplega, amb
-vo amb la seccióvolumes:de Compose.
Demostració del punt 3:
El fitxer no hi és. Es va escriure en una capa que el muntatge del volum tapa.
Per a Aurora Libros no es fa servir VOLUME en cap Dockerfile. L'API no desa estat —llegeix de PostgreSQL i posa a la memòria cau amb Redis—, i la persistència del catàleg es resol muntant un volum en executar el contenidor d'aurora-db, que és el tema de la lliçó 03-06. Curiosament, la imatge oficial de PostgreSQL sí que declara VOLUME /var/lib/postgresql/data, i és justament el motiu que apareguin volums anònims orfes així que hi experimentes sense -v.
STOPSIGNAL i la gestió de senyals
STOPSIGNAL i la gestió de senyalsSTOPSIGNAL canvia el senyal que Docker envia al PID 1 en executar docker stop:
STOPSIGNAL SIGTERM # El valor per defecte
STOPSIGNAL SIGQUIT # El que necessita Nginx
STOPSIGNAL SIGINT
STOPSIGNAL 15 # També per númeroEl comportament per defecte ja el coneixes de la lliçó 02-03: docker stop envia SIGTERM, espera 10 segons i envia SIGKILL, que no es pot capturar ni ignorar.
El problema és que no tots els programes interpreten SIGTERM igual:
| Programa | SIGTERM | Senyal per a aturada ordenada |
|---|---|---|
| Node.js | Acaba immediatament | SIGTERM (amb gestor propi) |
| Nginx | Aturada ràpida: talla les connexions en curs | SIGQUIT: aturada ordenada |
| PostgreSQL | Smart shutdown: espera els clients | SIGINT per a fast shutdown |
| Apache | Aturada immediata | SIGWINCH per a l'ordenada |
El cas de Nginx és el que afecta Aurora Libros: el contenidor aurora-web del mòdul 4 voldrà STOPSIGNAL SIGQUIT per no tallar peticions a mig servir durant un desplegament.
Per a l'API, SIGTERM (el valor per defecte) és correcte, però convé que server.js el capturi per tancar netament. El patró, que es reprendrà a la lliçó 06-01:
// Aturada ordenada: deixa d'acceptar connexions noves, acaba les
// en curs i tanca el pool de PostgreSQL i el client de Redis.
const servidor = app.listen(PORT, '0.0.0.0', () => { /* ... */ });
async function aturar(senyal) {
console.log(`[aurora-api] rebut ${senyal}, tancant ordenadament...`);
servidor.close(async () => {
await pool.end();
await cache.quit();
console.log('[aurora-api] tancat netament');
process.exit(0);
});
// Xarxa de seguretat: si alguna cosa s'encalla, sortir abans del SIGKILL
setTimeout(() => process.exit(1), 8000).unref();
}
process.on('SIGTERM', () => aturar('SIGTERM'));
process.on('SIGINT', () => aturar('SIGINT'));Fixa't en el temporitzador de 8 segons: és deliberadament inferior als 10 de docker stop, per forçar la sortida abans que arribi el SIGKILL. I recorda que res d'això no serveix si el CMD està en forma shell: el senyal no arribaria mai al procés de Node.
ONBUILD i SHELL
ONBUILD i SHELLDues instruccions d'ús minoritari que convé reconèixer.
ONBUILD
Registra una instrucció que no s'executa ara, sinó quan algú faci servir aquesta imatge com a base amb un FROM.
# A la imatge "aurora-base-node"
FROM node:22-alpine
WORKDIR /app
ONBUILD COPY package*.json ./
ONBUILD RUN npm ci --omit=dev && npm cache clean --force
ONBUILD COPY . .
CMD ["node", "server.js"]Qui la faci servir només escriu:
I en construir, es disparen automàticament els tres passos registrats. És una plantilla per estandarditzar microserveis d'una organització.
Per què es fa servir poc: la màgia oculta. Qui llegeix aquell Dockerfile d'una línia no veu el que passarà; ha d'anar a inspeccionar la base. Depurar una fallada en un ONBUILD és especialment frustrant, perquè l'error assenyala un fitxer que no conté la instrucció culpable. Les mateixes imatges oficials de Node van retirar les seves variants onbuild fa anys per aquest motiu. Si necessites estandarditzar, avui és preferible una plantilla de Dockerfile compartida o un fitxer base ben documentat.
SHELL
Canvia l'intèrpret que fa servir la forma shell de RUN, CMD i ENTRYPOINT:
Per defecte és ["/bin/sh", "-c"] a Linux. El cas d'ús més comú i legítim a Linux és activar el mode estricte de bash en imatges amb molts RUN encadenats:
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN curl -s https://exemple.com/llista.txt | grep aurora > /app/llista.txtSense pipefail, aquell RUN té èxit encara que el curl falli, perquè el codi de sortida d'una canonada és el de l'última comanda, i grep sí que funcionaria. Amb pipefail, la fallada de qualsevol baula fa fallar la instrucció sencera. És una fallada silenciosa clàssica que produeix imatges amb fitxers buits.
Nota per a Alpine: node:22-alpine no porta bash, només ash de BusyBox. Fer servir SHELL ["/bin/bash", …] allà requeriria RUN apk add --no-cache bash. Al Dockerfile d'aurora-api no apareix cap SHELL: no hi ha canonades als RUN.
- El Dockerfile professional d'
aurora-api
aurora-apiTot junt. Desa això a ~/aurora-libros/api/Dockerfile:
# syntax=docker/dockerfile:1
# =============================================================================
# aurora-api · API REST del catàleg d'Aurora Libros S.L.
#
# Construcció estàndard:
# docker build -t auroralibros/aurora-api:1.1.0 .
#
# Construcció amb metadades completes (el que farà el pipeline a 06-02):
# docker build \
# --build-arg VERSION=1.1.0 \
# --build-arg REVISION="$(git rev-parse --short HEAD)" \
# --build-arg CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
# -t auroralibros/aurora-api:1.1.0 .
# =============================================================================
# --- Arguments globals: només els veuen les instruccions FROM ----------------
# Permeten provar una altra versió de Node sense tocar el fitxer:
# docker build --build-arg NODE_VERSION=23 -t aurora-api:node23 .
ARG NODE_VERSION=22
ARG ALPINE_VERSION=3.21
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION}
# --- Arguments de l'etapa: metadades de traçabilitat ------------------------
# Reimportem NODE_VERSION perquè els ARG globals no travessen el FROM.
ARG NODE_VERSION
ARG VERSION=1.1.0
ARG REVISION=desconeguda
ARG CREATED=desconeguda
# --- Metadades OCI ----------------------------------------------------------
# Costen zero bytes i responen a "quin commit porta dins això?" a les 3 AM.
LABEL org.opencontainers.image.title="aurora-api" \
org.opencontainers.image.description="API REST del catàleg d'Aurora Libros S.L." \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.authors="[email protected]" \
org.opencontainers.image.vendor="Aurora Libros S.L." \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/auroralibros/aurora-libros" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${CREATED}" \
org.opencontainers.image.base.name="docker.io/library/node:${NODE_VERSION}-alpine"
WORKDIR /app
# --- Dependències abans que codi (memòria cau, lliçó 02-02) -----------------
# --chown al COPY mateix: evita un RUN chown -R posterior que duplicaria
# la mida de la capa per copy-on-write.
COPY --chown=node:node package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
# --- Codi de l'aplicació ----------------------------------------------------
COPY --chown=node:node . .
# --- Configuració per defecte, sobreescrivible amb -e -----------------------
# CAP SECRET AQUÍ: quedaria a les metadades i a docker image history.
ENV NODE_ENV=production \
PORT=3000 \
APP_VERSION=${VERSION}
# --- Usuari sense privilegis ------------------------------------------------
# Va DESPRÉS d'instal·lar i copiar (root necessitava escriure) i ABANS del CMD.
# node:22-alpine ja inclou l'usuari "node" amb uid 1000.
USER node
# --- Port: documentació, no publicació --------------------------------------
# 3000 i no 80 perquè un usuari sense privilegis no pot fer servir ports <1024.
EXPOSE 3000
# --- Comprovació de salut ---------------------------------------------------
# wget ve amb BusyBox a Alpine; curl no hi és i afegiria ~4 MB.
# start-period de 10 s per no marcar unhealthy durant una arrencada legítima.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000/salut || exit 1
# --- Procés principal -------------------------------------------------------
# ENTRYPOINT fixa l'executable; CMD són els arguments reemplaçables.
# Tots dos en forma exec: node és PID 1 i rep SIGTERM directament.
ENTRYPOINT ["node"]
CMD ["server.js"]Construeix la versió nova:
cd ~/aurora-libros/api
docker build \
--build-arg VERSION=1.1.0 \
--build-arg REVISION="$(git rev-parse --short HEAD 2>/dev/null || echo sense-git)" \
--build-arg CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-t auroralibros/aurora-api:1.1.0 .[+] Building 10.8s (13/13) FINISHED
=> [1/5] FROM docker.io/library/node:22-alpine3.21@sha256:9f2c... 0.0s
=> [2/5] WORKDIR /app 0.1s
=> [3/5] COPY --chown=node:node package*.json ./ 0.0s
=> [4/5] RUN npm ci --omit=dev && npm cache clean --force 8.7s
=> [5/5] COPY --chown=node:node . . 0.1s
=> exporting to image 0.7s
=> => naming to docker.io/auroralibros/aurora-api:1.1.0 0.0sVerificació completa de tot el que has afegit:
# 1. L'usuari ja no és root
docker run --rm auroralibros/aurora-api:1.1.0 --eval "console.log(process.getuid())"Fixa't en el detall: amb ENTRYPOINT ["node"], l'argument --eval "..." ha substituït el CMD ["server.js"] i s'ha executat node --eval .... El cas 3 de la taula de l'apartat 3, en directe.
# 2. ENTRYPOINT i CMD a les metadades
docker image inspect auroralibros/aurora-api:1.1.0 \
--format 'Entrypoint: {{json .Config.Entrypoint}}
Cmd: {{json .Config.Cmd}}
User: {{.Config.User}}
Health: {{json .Config.Healthcheck.Test}}'Entrypoint: ["node"]
Cmd: ["server.js"]
User: node
Health: ["CMD-SHELL","wget --quiet --tries=1 --spider http://localhost:3000/salut || exit 1"]# 3. Arrencada, healthcheck i aturada neta
docker run -d --name aurora-pro -p 3000:3000 auroralibros/aurora-api:1.1.0
sleep 5 && docker ps --filter name=aurora-pro --format "{{.Names}}: {{.Status}}"
sleep 40 && docker ps --filter name=aurora-pro --format "{{.Names}}: {{.Status}}"
time docker stop aurora-pro
docker rm aurora-proaurora-pro: Up 5 seconds (health: starting)
aurora-pro: Up 45 seconds (unhealthy)
aurora-pro
real 0m0.304sLes tres coses confirmades: període de gràcia, detecció correcta que el servei no està operatiu (falta PostgreSQL, mòdul 3) i aturada en 0,3 segons gràcies a la forma exec.
Compara amb la versió anterior:
| Aspecte | 1.0.0 (lliçó 02-03) |
1.1.0 (aquesta lliçó) |
|---|---|---|
| Usuari d'execució | root (uid 0) |
node (uid 1000) |
| Versió de Node | Fixa al FROM |
Parametritzable amb --build-arg |
| Traçabilitat | Cap | 11 etiquetes OCI, amb commit i data |
| Salut del servei | Només "el procés viu" | healthy/unhealthy real contra /salut |
| Executable | Reemplaçable del tot | Fix (node), amb arguments reemplaçables |
| Mida | 167 MB | 167 MB (les metadades no pesen) |
Tota aquesta millora ha costat zero bytes.
Errors Habituals i Consells
- Fer servir
ARGper a secrets. Queden adocker image historyen clar. Per a credencials a la build existeixen els secrets de BuildKit (lliçó 05-05); per a credencials en execució,-ei fitxers d'entorn (lliçó 04-05). - Esperar que un
ARGglobal estigui disponible dins de l'etapa. ElsARGanteriors al primerFROMnomés els veuen elsFROM. Cal tornar a declarar-los, sense valor, dins de l'etapa. ENTRYPOINTen forma shell. Trenca dues coses: el PID 1 passa a sersh(10 s d'aturada) i els arguments dedocker runi elCMDs'ignoren completament. Forma exec sempre.- Un script d'entrypoint sense
exec. L'script es queda com a PID 1, no reenvia SIGTERM i tornes als 10 segons i el SIGKILL. L'última línia ha de serexec "$@", amb cometes. USERmassa amunt. Si canvies d'usuari abans denpm ci, el pas falla ambEACCES. Instal·la com a root, copia amb--chowni posaUSERjust abans delCMD.RUN chown -Rdesprés de copiar. Duplica la mida d'aquella capa pel copy-on-write. Fes servirCOPY --chown.- Un usuari sense privilegis escoltant al port 80.
EACCES: permission denied. Escolta en un port alt i publica amb-p 80:3000. HEALTHCHECKsense--start-period. El contenidor es marcaunhealthydurant la seva arrencada legítima i l'orquestrador entra en un bucle de reinicis.- Un healthcheck que consulta dependències externes i no distingeix. Si
/salutretorna 503 perquè la base de dades està caiguda, l'orquestrador reiniciarà l'API, que no en té cap culpa. Per a Aurora Libros és correcte en desenvolupament; en producció convé separar liveness (viu el procés?) de readiness (pot atendre?), i això es veu a la lliçó 06-05. VOLUMEal Dockerfile. Genera volums anònims orfes, no es pot desfer i fa desaparèixer silenciosament el que escriguis després en aquella ruta. Decideix l'emmagatzematge en executar.- Consell: posa els
LABELen una sola instrucció, amb\per partir línies. CadaLABELés una capa de metadades. - Consell: prova la comanda del healthcheck a mà amb
docker execabans de ficar-la al Dockerfile. Estalvia cicles de build sencers.
Exercicis
Exercici 1: demostra la diferència entre ARG i ENV
Construeix una imatge basada en alpine:3.21 que:
- Rebi un
ARG ENTORN_BUILDamb valor per defectelocal. - Defineixi un
ENV ENTORN_RUNa partir d'aquellARG. - Imprimeixi tots dos valors durant la construcció amb un
RUN. - Imprimeixi tots dos en executar el contenidor.
Després:
- Construeix-la sense
--build-argi executa-la. Què imprimeix cada variable? - Construeix-la amb
--build-arg ENTORN_BUILD=produccioi executa-la. Què canvia? - Executa la imatge amb
-e ENTORN_RUN=sobreescrit. Es pot fer el mateix amb l'ARG? - Comprova amb
docker image historysi el valor de l'ARGés visible. Explica què implica això per a les contrasenyes.
Exercici 2: recorre les quatre combinacions d'ENTRYPOINT i CMD
Crea quatre imatges basades en alpine:3.21, una per fila de la taula de l'apartat 3, fent servir echo com a executable. Per a cadascuna, executa docker run --rm <imatge> i docker run --rm <imatge> adeu i anota la sortida real. Després:
- Omple la taula amb el que has observat i compara-la amb la teòrica.
- Explica en una frase la regla que governa les quatre files.
- Fes servir
--entrypointper executarls /a la imatge de la quarta fila.
Exercici 3: healthcheck que sí que distingeix
L'HEALTHCHECK actual d'aurora-api marca el contenidor unhealthy quan falla PostgreSQL, encara que l'API funcioni perfectament. Dissenya'n una alternativa:
- Explica per què això és problemàtic en un orquestrador que reinicia els contenidors malalts.
- Proposa dos endpoints diferents i què hauria de comprovar cadascun.
- Escriu l'
HEALTHCHECKque faries servir al Dockerfile i justifica'n l'elecció. - Demostra amb un contenidor real la diferència entre comprovar
/salut(que retorna 503 sense base de dades) i comprovar només que el port respon.
Solucions
Solució a l'exercici 1
mkdir -p /tmp/ex-argenv && cd /tmp/ex-argenv
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.21
ARG ENTORN_BUILD=local
ENV ENTORN_RUN=${ENTORN_BUILD}
RUN echo "[build] ARG=${ENTORN_BUILD} ENV=${ENTORN_RUN}"
CMD ["sh", "-c", "echo \"[run] ARG=[${ENTORN_BUILD}] ENV=[${ENTORN_RUN}]\""]
EOF
docker build --no-cache --progress=plain -t ex:defecte . 2>&1 | grep "\[build\]"
docker run --rm ex:defecteAmb --build-arg:
docker build --no-cache --progress=plain --build-arg ENTORN_BUILD=produccio -t ex:prod . 2>&1 | grep "\[build\]"
docker run --rm ex:prodAnàlisi:
- A la build, totes dues existeixen. L'
ENVpren el valor de l'ARG: és el patró pont. - En execució, l'
ARGestà sempre buit. Mai no va arribar al contenidor. - L'
ENVconserva el valor congelat a la build.
Sobreescriptura en execució:
L'ENV sí que se sobreescriu amb -e. L'ARG no, perquè no existeix en execució: -e ENTORN_BUILD=x crearia una variable d'entorn nova sense cap relació amb l'ARG de la build.
L'historial:
CMD ["sh" "-c" "echo \"[run] ARG=[${ENTORN_BUILD}] ENV=[${ENTORN_RUN}]\""]
|1 ENTORN_BUILD=produccio /bin/sh -c echo "[build] ARG=${ENTORN_BUILD} ENV=${ENTORN_RUN}"
ENV ENTORN_RUN=produccio
ARG ENTORN_BUILD=localEl valor produccio apareix en clar a la línia del RUN. Si en lloc de produccio hagués estat superSecreta2026, qualsevol que descarregués la imatge la llegiria amb una sola comanda i sense arrencar res. Aquesta és exactament la raó per la qual ARG no serveix per a secrets, per molt que "desaparegui" en execució: desapareix de l'entorn, no de l'historial.
Solució a l'exercici 2
mkdir -p /tmp/ex-ep && cd /tmp/ex-ep
printf 'FROM alpine:3.21\nCMD ["echo", "hola-cmd"]\n' > D1
printf 'FROM alpine:3.21\nENTRYPOINT ["echo", "hola-entry"]\n' > D2
printf 'FROM alpine:3.21\nENTRYPOINT ["echo"]\nCMD ["hola-combi"]\n' > D3
printf 'FROM alpine:3.21\nENTRYPOINT ["echo", "prefix"]\nCMD ["sufix"]\n' > D4
for n in 1 2 3 4; do docker build -q -t ep:$n -f D$n . ; done
for n in 1 2 3 4; do
echo "--- Cas $n ---"
echo -n "sense arguments: "; docker run --rm ep:$n
echo -n "amb 'adeu': "; docker run --rm ep:$n adeu
done--- Cas 1 ---
sense arguments: hola-cmd
amb 'adeu': /bin/sh: adeu: not found
--- Cas 2 ---
sense arguments: hola-entry
amb 'adeu': hola-entry adeu
--- Cas 3 ---
sense arguments: hola-combi
amb 'adeu': adeu
--- Cas 4 ---
sense arguments: prefix sufix
amb 'adeu': prefix adeu1. La taula observada coincideix amb la teòrica. El cas 1 amb argument és especialment il·lustratiu: adeu va reemplaçar el CMD sencer i Docker va intentar executar un programa anomenat adeu, que no existeix. Al cas 2, adeu es va afegir a l'ENTRYPOINT. Al 3 i al 4, adeu va substituir el CMD però l'ENTRYPOINT va romandre.
2. La regla: ENTRYPOINT és l'executable fix i CMD són els seus arguments per defecte; el que escrius després del nom de la imatge a docker run reemplaça únicament el CMD.
3. Amb --entrypoint:
--entrypoint ls substitueix echo, i la / posterior al nom de la imatge substitueix el CMD, i queda ls /. És la comanda que necessitaràs cada vegada que vulguis inspeccionar una imatge amb ENTRYPOINT propi:
Solució a l'exercici 3
1. El problema. El healthcheck actual comprova /salut, que retorna 503 si PostgreSQL o Redis no responen. A Swarm o Kubernetes, un contenidor unhealthy es reinicia o es reemplaça. Si la base de dades cau cinc minuts, totes les rèpliques de l'API es marcaran malaltes i entraran en un cicle de reinicis tot i estar perfectament sanes. Pitjor encara: quan PostgreSQL torni, es trobarà amb una allau de reconnexions de rèpliques acabades d'arrencar, amb les memòries cau buides. S'ha convertit una fallada d'una dependència en una caiguda total i en una tempesta de reintents.
2. Dos endpoints amb responsabilitats diferents:
| Endpoint | Pregunta que respon | Comprova | Acció si falla |
|---|---|---|---|
/salut/viu (liveness) |
El procés funciona? | Només que Express respon. Sense dependències | Reiniciar: el procés està trencat |
/salut/preparat (readiness) |
Pot atendre peticions? | Base de dades i memòria cau accessibles | Treure del balanceig, sense reiniciar |
La distinció és la clau: reiniciar arregla un procés encallat, però no arregla una base de dades caiguda. Davant d'una fallada de dependència, el correcte és deixar d'enviar trànsit a aquella rèplica i esperar, no matar-la.
3. L'HEALTHCHECK del Dockerfile:
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000/salut/viu || exit 1Justificació: l'HEALTHCHECK de Docker té un únic estat, i la seva conseqüència natural és el reinici o el reemplaçament. Per tant ha de reflectir la liveness. La readiness la consulta el balancejador o l'orquestrador pel seu compte —a Compose, amb depends_on: condition: service_healthy (lliçó 04-02); a Kubernetes, amb una readinessProbe separada (lliçó 06-05)—.
L'endpoint nou a server.js seria tan simple com:
// Liveness: no toca cap dependència externa a propòsit
app.get('/salut/viu', (req, res) => res.status(200).json({ estat: 'viu' }));4. La demostració. Amb el healthcheck actual, sense base de dades:
docker run -d --name hc-salut auroralibros/aurora-api:1.1.0
sleep 45
docker ps --filter name=hc-salut --format "{{.Names}}: {{.Status}}"Ara sobreescrivint el healthcheck en temps d'execució per comprovar només que el port respon:
docker run -d --name hc-port \
--health-cmd="wget -q --tries=1 --spider http://localhost:3000/llibres/abc || exit 1" \
--health-interval=10s --health-start-period=10s --health-retries=3 \
auroralibros/aurora-api:1.1.0
sleep 45
docker ps --filter name=hc-port --format "{{.Names}}: {{.Status}}"El mateix contenidor, amb la mateixa base de dades absent, és unhealthy amb una comprovació i healthy amb l'altra. La petició a /llibres/abc retorna un 400 de validació —sense tocar PostgreSQL, perquè l'identificador no és un enter—, cosa que demostra que Express és viu i encaminant correctament. És exactament el senyal de liveness que es busca.
Fixa't de passada en les opcions --health-* de docker run: permeten sobreescriure el healthcheck de la imatge sense reconstruir-la, una cosa utilíssima per experimentar.
Conclusió
La teva imatge ha passat de "funciona" a "és professional", i sense guanyar ni un sol byte. Saps distingir ARG d'ENV pel seu àmbit i el seu moment —el primer s'evapora en acabar la build, el segon viatja dins del contenidor—, i saps que cap dels dos no serveix per a secrets, perquè un --build-arg amb una contrasenya queda escrit en clar a docker image history. Has fet servir ARG abans del FROM per parametritzar la versió de Node i poder provar Node 23 sense editar ni una línia.
Domines el binomi ENTRYPOINT + CMD i les seves quatre combinacions, amb la regla que les resumeix: l'ENTRYPOINT és l'executable fix, el CMD són els arguments reemplaçables, i --entrypoint és la teva porta d'entrada quan necessites depurar. Has vist per què un script docker-entrypoint.sh ha d'acabar en exec "$@": sense aquell exec, l'script es queda com a PID 1, no reenvia SIGTERM i tornes als deu segons d'espera i el SIGKILL. Amb USER node has deixat d'executar l'API com a root —uid=1000 confirmat— instal·lant primer com a root i copiant amb --chown per no duplicar capes, i entens per què això obliga a escoltar al 3000 i no al 80.
Amb onze etiquetes OCI la imatge ja diu qui la va fer, de quin commit surt i quan es va construir, que és el que necessites a les tres de la matinada. Amb HEALTHCHECK Docker vigila l'endpoint /salut que vas preparar a la lliçó 01-07, i has vist el cicle complet starting → unhealthy a docker ps, amb l'historial de fallades a docker inspect. I saps per què VOLUME al Dockerfile sol ser mala idea —volums orfes, decisió irreversible imposada a l'usuari i escriptures posteriors que desapareixen sense avís—, per a què serveix STOPSIGNAL (Nginx necessita SIGQUIT) i què fan ONBUILD i SHELL en els pocs casos en què valen la pena.
auroralibros/aurora-api:1.1.0 és una imatge que s'executa com a usuari sense privilegis, s'identifica, s'autodiagnostica i s'atura netament en 0,3 segons. Ja saps construir-la; ara convé saber administrar-la. A la lliçó següent, Gestionant Imatges Docker, t'ocuparàs del magatzem local: llistar i filtrar amb plantilles, extreure camps concrets amb inspect --format, auditar amb history quina capa s'ha menjat 80 MB, esborrar imatges amb contenidors que les fan servir, entendre d'on surten aquelles misterioses imatges <none>:<none> que s'acumulen build rere build, netejar amb la família prune sense destruir el que no toca, diagnosticar l'espai amb docker system df i endur-te una imatge a una altra màquina sense cap registre, amb docker save i docker load.
Docker: De Principiant a Avançat
Mòdul 1: Introducció a Docker
- Què és Docker?
- Instal·lant Docker
- Arquitectura de Docker
- Comandes Bàsiques de Docker
- Entenent les Imatges de Docker
- Creant el teu Primer Contenidor Docker
- El Projecte del Curs: la Plataforma Aurora Libros
Mòdul 2: Treballant amb Imatges Docker
- Docker Hub i Repositoris
- Construint Imatges Docker
- Conceptes Bàsics de Dockerfile
- Instruccions Avançades del Dockerfile
- Gestionant Imatges Docker
- Etiquetatge i Publicació d'Imatges
Mòdul 3: Contenidors Docker
- Executant Contenidors
- Cicle de Vida del Contenidor
- Gestionant Contenidors
- Inspecció i Depuració de Contenidors
- Xarxes a Docker
- Persistència de Dades amb Volums
- Límits de Recursos i Polítiques de Reinici
Mòdul 4: Docker Compose
- Introducció a Docker Compose
- Definint Serveis a Docker Compose
- Comandes de Docker Compose
- Aplicacions Multi-Contenidor
- Variables d'Entorn a Docker Compose
- Perfils, Overrides i Múltiples Entorns
- Desenvolupament Local amb Docker Compose
Mòdul 5: Conceptes Avançats de Docker
- Aprofundiment en Xarxes Docker
- Opcions d'Emmagatzematge Docker
- Millors Pràctiques de Seguretat a Docker
- Optimitzant Imatges Docker
- Builds Avançades amb BuildKit i Buildx
- Registre i Monitoratge a Docker
- El Runtime per Dins: Namespaces, Cgroups i Capes
Mòdul 6: Docker en Producció
- Preparar una Imatge per a Producció
- CI/CD amb Docker
- Orquestrant Contenidors amb Docker Swarm
- Introducció a Kubernetes
- Desplegant Contenidors Docker a Kubernetes
- Escalat i Balanceig de Càrrega
- Estratègies de Desplegament i Rollback
