A la lliçó anterior vas construir auroralibros/aurora-api:0.1.0 amb un Dockerfile que se't va donar fet i se't va explicar per sobre. Funciona, arrenca l'API dins d'un contenidor i respon a curl. Però el vas escriure gairebé a cegues: saps què fa cada línia, no per què està escrita exactament així. Aquesta lliçó salda aquell deute. Recorreràs el llenguatge del Dockerfile instrucció per instrucció, amb el nivell de detall que a 02-02 va quedar pendent: per què WORKDIR és millor que RUN cd, en què es diferencien COPY i ADD i per què gairebé sempre guanya COPY, què signifiquen exactament les dues formes de RUN i de CMD, per què encadenar comandes amb && produeix imatges més petites, per què EXPOSE no exposa res, i què li passa al teu contenidor quan el pares segons com hagis escrit el CMD. Al final construiràs el Dockerfile definitiu d'aurora-api, comentat línia a línia i amb cada decisió justificada.
Contingut
- Estructura d'un Dockerfile, comentaris i la directiva
# syntax - Taula resum de les instruccions bàsiques
FROM: d'on parteixesWORKDIR: on treballesCOPY: què portes del contextCOPYdavant d'ADDRUN: què executes durant la construccióENV: configuració que sobreviu a la construccióEXPOSE: documentació, no publicacióCMD: quin procés arrenca el contenidor- El Dockerfile definitiu d'
aurora-api
- Estructura d'un Dockerfile, comentaris i la directiva
# syntax
# syntaxUn Dockerfile és un fitxer de text pla, sense extensió, amb una instrucció per línia:
# syntax=docker/dockerfile:1
# Comentari: BuildKit l'ignora completament
FROM node:22-alpine
WORKDIR /app
RUN echo "una instrucció" && \
echo "pot continuar a la línia següent"
CMD ["node", "server.js"]Regles de sintaxi:
- Les instruccions s'escriuen en MAJÚSCULES per convenció.
from node:22-alpinefunciona igual, però ningú no ho escriu així: en majúscules es distingeix d'un cop d'ull la instrucció dels seus arguments. - Una instrucció per línia. Per partir una línia llarga es fa servir la barra invertida
\al final, sense res després (ni tan sols un espai: un espai després de la barra n'inverteix l'efecte i produeix errors desconcertants). - Les línies que comencen per
#són comentaris, llevat de les directives de l'apartat següent. Com vas comprovar a l'exercici 2 de la lliçó 02-02, els comentaris no afecten la memòria cau. - L'ordre importa, i no només per la memòria cau: cada instrucció s'executa sobre l'estat que va deixar l'anterior.
- Les línies buides s'ignoren. Fes-les servir per agrupar blocs lògics; un Dockerfile ben espaiat es llegeix moltíssim millor.
La directiva # syntax
La primera línia del fitxer mereix un apartat propi:
No és un comentari: és una directiva de l'intèrpret. Li diu a BuildKit quina versió del frontend de Dockerfile ha de fer servir per interpretar la resta del fitxer. BuildKit descarrega aquella imatge del registre i la fa servir com a analitzador sintàctic.
Per què convé posar-la sempre:
- Et dona les funcionalitats més recents sense actualitzar Docker.
docker/dockerfile:1és una etiqueta mòbil que apunta a l'última versió estable de la sèrie 1. Coses comCOPY --link, els muntatges de memòria cau o els secrets de build (lliçó 05-05) en depenen. - Fa el fitxer portable. El mateix Dockerfile s'interpreta igual al teu portàtil i en un agent de CI amb una altra versió de Docker.
- És compatible cap enrere. La sèrie
1garanteix que no trencarà els Dockerfiles existents.
Les variants que veuràs:
| Directiva | Què obtens |
|---|---|
# syntax=docker/dockerfile:1 |
Última estable de la sèrie 1. La recomanada |
# syntax=docker/dockerfile:1.12 |
Fixada a una versió menor concreta, per a màxima reproductibilitat |
| (sense directiva) | El frontend que porti la teva versió de Docker Engine. Funciona, però perds funcionalitats |
Ha de ser la primera línia del fitxer (abans de qualsevol instrucció, fins i tot abans d'altres comentaris) perquè faci efecte.
- Taula resum de les instruccions bàsiques
Aquestes són les instruccions que cobreix aquesta lliçó, amb l'essencial de cadascuna:
| Instrucció | Què fa | Crea capa? | Quan actua? |
|---|---|---|---|
FROM |
Estableix la imatge base | Hereta les de la base | Construcció |
WORKDIR |
Fixa el directori de treball | Sí (metadada, mida ~0) | Construcció i execució |
COPY |
Copia fitxers del context a la imatge | Sí | Construcció |
ADD |
Com COPY, més URL i descompressió automàtica |
Sí | Construcció |
RUN |
Executa una comanda i congela el resultat | Sí | Construcció |
ENV |
Defineix variables d'entorn | Sí (metadada) | Construcció i execució |
EXPOSE |
Documenta el port que fa servir el servei | Sí (metadada) | Només documentació |
CMD |
Defineix el procés per defecte del contenidor | Sí (metadada) | Execució |
La columna "Crea capa?" explica una observació de la lliçó 01-05: no totes les instruccions engreixen la imatge. COPY, ADD i RUN modifiquen el sistema de fitxers i generen capes amb contingut real; les altres només escriuen metadades al manifest, i la seva capa pesa zero bytes.
La columna "Quan actua?" és la que evita més confusió. RUN s'executa en construir i el seu resultat queda congelat; CMD no s'executa en construir en absolut, només descriu què farà el contenidor en arrencar. Confondre-les és l'error conceptual número u de qui comença.
Hi ha més instruccions —ARG, ENTRYPOINT, USER, LABEL, HEALTHCHECK, VOLUME, STOPSIGNAL, ONBUILD, SHELL—, i totes es veuen a la lliçó següent, la 02-04.
FROM: d'on parteixes
FROM: d'on parteixesFROM és obligatòria i ha de ser la primera instrucció (llevat de directives i comentaris). Estableix el sistema de fitxers i les metadades de partida: tot el que vingui després es construeix a sobre d'aquelles capes.
Sintaxi completa:
Formes que veuràs, aplicades a Aurora Libros:
FROM node # Perillós: latest, versió impredictible
FROM node:22 # Millor: fixa la major, però la imatge és de ~1,1 GB
FROM node:22-alpine # La triada: lleugera i amb la major fixada
FROM node:22.14-alpine3.21 # Màxim control: fixa menor i versió d'Alpine
FROM node:22-alpine@sha256:9f2c1a... # Reproductibilitat absoluta: digest immutableEl compromís entre reproductibilitat i manteniment és real i no té una resposta única:
| Forma | Reproductibilitat | Pedaços de seguretat | Recomanat per a |
|---|---|---|---|
node (= latest) |
Nul·la | Automàtics, però pot canviar de versió major sense avisar | Mai |
node:22 |
Mitjana | Automàtics dins de la major 22 | Desenvolupament |
node:22-alpine |
Mitjana | Automàtics dins de la major 22 | Aurora Libros: bon equilibri |
node:22.14-alpine3.21 |
Alta | Manuals | Entorns regulats |
@sha256:… |
Total | Manuals | Producció crítica, cadena de subministrament auditada |
Aurora Libros fa servir node:22-alpine, decidit a la lliçó 01-07 per tres motius que ara pots justificar del tot: compleix l'engines: node >=22.0.0 del package.json, és imatge oficial (espai library/, primer criteri de confiança de la lliçó 02-01) i pesa ~142 MB davant dels ~1,1 GB de node:22. La contrapartida és que Alpine fa servir musl en lloc de glibc com a biblioteca C; les tres dependències del projecte (express, pg, redis) són JavaScript pur o porten binaris compatibles, així que no suposa cap problema.
La clàusula AS <nom> dona nom a una etapa de construcció. Serveix per a les builds multietapa, la tècnica que permet compilar en una imatge i endur-se'n només el resultat a una altra molt més petita. S'estudia a la lliçó 05-04; menciona-la mentalment i continua.
WORKDIR: on treballes
WORKDIR: on treballesWORKDIR fixa el directori de treball per a totes les instruccions posteriors que el facin servir: RUN, COPY, ADD i CMD. Si no existeix, el crea, inclosos els directoris intermedis.
Per què no RUN cd
És la pregunta obligada. Compara:
En el primer cas, npm ci s'executa a /, no a /app, i falla amb ENOENT: no such file or directory, open '/package.json'. La raó és fonamental: cada RUN s'executa en un contenidor temporal nou. El cd canvia el directori de l'intèrpret d'ordres d'aquell contenidor efímer, que mor així que acaba la instrucció; el RUN següent arrenca de zero, al directori de treball heretat de la imatge.
L'única manera que cd serveixi és encadenar-lo dins del mateix RUN:
Tot i així, WORKDIR guanya per quatre raons:
- Persisteix en temps d'execució. És el directori on entres amb
docker exec -it aurora-api shi on s'executa elCMD. Uncddins d'unRUNno deixa rastre. - Crea el directori si no existeix, sense necessitat de
mkdir -p. - És una metadada visible.
docker image inspectet diu quin és; uncdenterrat en unRUNs'ha d'anar a buscar. - Es llegeix millor. Declara la intenció en lloc d'amagar-la dins d'una cadena de comandes.
Detalls addicionals:
WORKDIR /app # Absoluta: sempre preferible
WORKDIR api # Relativa a l'anterior: acaba a /app/api
WORKDIR $DIRECTORI # Admet variables definides amb ENV o ARGFes servir sempre rutes absolutes. Les relatives encadenades obliguen a portar el compte mental d'on ets i són una font clàssica d'errors en Dockerfiles llargs.
Per a Aurora Libros, /app és la convenció habitual en imatges de Node. Qualsevol ruta serviria (/srv/aurora, /usr/src/app), però /app és curta, inequívoca i no col·lisiona amb el sistema de fitxers d'Alpine.
COPY: què portes del context
COPY: què portes del contextCOPY porta fitxers i directoris des del context de construcció fins al sistema de fitxers de la imatge.
Regles que cal tenir clares:
- L'origen és sempre relatiu a l'arrel del context, mai al teu directori actual ni a la ubicació del Dockerfile. I mai no pot sortir del context: és la restricció que va provocar l'error
"/db/init.sql": not foundde la lliçó 02-02. - La destinació és relativa al
WORKDIRsi no és absoluta. AmbWORKDIR /app, la destinació./significa/app/. - Si la destinació acaba en
/, es tracta com a directori; si no, i hi ha un sol origen, s'interpreta com el nom del fitxer de destinació. Posar sempre la barra final evita sorpreses. - Amb diversos orígens, la destinació ha de ser un directori i acabar en
/. COPYcopia el contingut d'un directori, no el directori.COPY web/ /public/deixa el contingut deweb/directament a/public/, no a/public/web/. És la font número u de rutes que "no apareixen" dins de la imatge.
Exemples amb Aurora Libros:
# Dos fitxers concrets al WORKDIR
COPY package.json package-lock.json ./
# Comodins: tot el que comenci per "package" i acabi en ".json"
COPY package*.json ./
# Tot el context (ja filtrat pel .dockerignore)
COPY . .
# Reanomenant a la destinació
COPY server.js /app/main.js
# Un directori complet a una ruta absoluta
COPY web/ /usr/share/nginx/html/Sobre COPY package*.json ./: el comodí cobreix package.json i package-lock.json d'una sola vegada. Té un avantatge pràctic sobre anomenar-los per separat: no falla si package-lock.json no existeix, perquè el patró simplement coincideix amb menys fitxers. En canvi, COPY package.json package-lock.json ./ dona error si falta el lock. Totes dues formes són defensables: l'explícita detecta abans l'oblit del lock (que trencaria el npm ci), la del comodí és més tolerant. Aurora Libros farà servir la del comodí, que és la convenció més estesa a l'ecosistema Node.
--chown i --chmod
Per defecte, tot el que es copia pertany a root:root. --chown canvia el propietari en la mateixa operació:
La imatge node:22-alpine ja inclou un usuari node sense privilegis. Copiar directament amb la seva propietat evita un RUN chown -R node:node /app posterior, que duplicaria la mida d'aquella capa: canviar els permisos d'un fitxer el marca com a modificat i el copy-on-write de la lliçó 01-05 obliga a escriure una còpia sencera a la capa nova. Un chown recursiu sobre node_modules pot afegir desenes de megues a la imatge.
--chmod fa el mateix amb els permisos:
L'ús d'USER per executar el contenidor sense privilegis es veu a la lliçó 02-04, i el perquè en profunditat, a la 05-03.
COPY davant d'ADD
COPY davant d'ADDADD existeix des del primer dia de Docker i fa tot el que fa COPY, més dues coses extra. Precisament per això convé evitar-la.
| Aspecte | COPY |
ADD |
|---|---|---|
| Copiar fitxers locals del context | Sí | Sí |
| Copiar directoris | Sí | Sí |
--chown / --chmod |
Sí | Sí |
Descomprimir automàticament un .tar, .tar.gz, .tar.bz2, .tar.xz local |
No | Sí |
| Descarregar des d'una URL | No | Sí (però desaconsellat) |
Clonar un repositori Git (--keep-git-dir) |
No | Sí, versions recents |
| Comportament predictible | Total | Depèn del tipus de fitxer |
| Recomanació oficial | Fer servir aquesta | Només en casos concrets |
El problema d'ADD és que el seu comportament depèn del que li passis:
ADD dades.tar.gz /app/ # Descomprimeix el tar dins de /app/
ADD dades.zip /app/ # NO descomprimeix: els zip no entren a la regla
ADD fitxer.txt /app/ # Còpia normalTres comportaments diferents amb la mateixa instrucció. Qui llegeix el Dockerfile ha de saber de memòria quins formats es descomprimeixen per predir-ne el resultat. COPY sempre fa exactament el mateix: copiar.
I el cas de la URL és pitjor:
# ❌ MALAMENT: descarrega un fitxer remot en una capa
ADD https://exemple.com/eina.tar.gz /tmp/
RUN tar -xzf /tmp/eina.tar.gz -C /opt && rm /tmp/eina.tar.gzQuatre problemes acumulats: el .tar.gz queda per sempre a la capa de l'ADD encara que després l'esborris (copy-on-write, lliçó 01-05), no pots verificar la suma de comprovació abans de fer-lo servir, ADD no descomprimeix el que ve d'una URL (només el local), i no controles capçaleres ni autenticació. La forma correcta, tot en un únic RUN:
# ✅ BÉ: descarrega, verifica, extreu i esborra a la MATEIXA capa
RUN wget -q https://exemple.com/eina.tar.gz -O /tmp/e.tar.gz && \
echo "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b /tmp/e.tar.gz" | sha256sum -c - && \
tar -xzf /tmp/e.tar.gz -C /opt && \
rm /tmp/e.tar.gzAquí sí que verifiques el hash, i l'esborrat passa dins de la mateixa capa, així que el fitxer temporal no engreixa la imatge.
La regla pràctica: fes servir COPY sempre. L'únic cas on ADD aporta alguna cosa genuïna és descomprimir un tarball local del context, que estalvia haver de tenir tar a la imatge:
Al Dockerfile d'aurora-api no apareix cap ADD: no hi ha tarballs ni descàrregues.
RUN: què executes durant la construcció
RUN: què executes durant la construccióRUN executa una comanda en el moment de construir la imatge, dins d'un contenidor temporal basat en les capes anteriors, i congela el sistema de fitxers resultant en una capa nova. És la instrucció que instal·la paquets, compila, genera fitxers… i la que més pes afegeix.
Forma shell i forma exec
# Forma shell: s'executa a través de /bin/sh -c
RUN npm ci --omit=dev
# Forma exec: s'executa directament, sense intèrpret d'ordres
RUN ["npm", "ci", "--omit=dev"]| Aspecte | Forma shell | Forma exec |
|---|---|---|
| Sintaxi | RUN comanda args |
RUN ["executable", "arg1", "arg2"] |
| Executa a través de | /bin/sh -c |
Directament |
Variables d'entorn ($VAR) |
S'expandeixen | No s'expandeixen |
Canonades, &&, >, * |
Funcionen | No funcionen |
| Requereix intèrpret d'ordres a la imatge | Sí | No |
Ús habitual a RUN |
El normal | Rar |
Per a RUN, la forma shell és l'habitual, perquè gairebé sempre vols encadenar comandes o expandir variables. La forma exec només cal en imatges sense intèrpret d'ordres (scratch, distroless) o quan un argument conté caràcters que l'intèrpret interpretaria malament.
Compte amb les cometes: la forma exec és JSON, així que exigeix cometes dobles. RUN ['npm', 'ci'] amb cometes simples no és JSON vàlid i BuildKit el tractarà com a forma shell, amb resultats desconcertants.
Per què encadenar amb && i \
Aquesta és la part que de debò importa. Cada RUN crea una capa. Compara:
# ❌ MALAMENT: quatre capes, i la brossa hi queda dins per sempre
RUN apk update
RUN apk add --no-cache curl
RUN apk add --no-cache tzdata
RUN rm -rf /var/cache/apk/*# ✅ BÉ: una sola capa, la neteja sí que fa efecte
RUN apk update && \
apk add --no-cache curl tzdata && \
rm -rf /var/cache/apk/*Dos problemes a la versió dolenta, i el segon és el greu:
- Quatre capes on n'hi hauria prou amb una. Més metadades, més entrades al manifest, més lentitud en muntar la imatge.
- El
rm -rfdel final no allibera res. Recorda de la lliçó 01-05: les capes són diffs i només s'apilen. Esborrar un fitxer a la capa 4 no l'elimina de la capa 2; només escriu un marcador d'esborrat que l'oculta. El fitxer continua viatjant dins de la imatge, ocupant el seu espai a cada descàrrega.
Comprova-ho tu mateix:
# Versió amb la neteja en un RUN a part
printf 'FROM alpine:3.21\nRUN apk add --no-cache python3\nRUN rm -rf /usr/lib/python3.12\n' > /tmp/Dockerfile.mal
docker build -q -t prova:mal -f /tmp/Dockerfile.mal /tmp
# Versió amb tot a la mateixa capa
printf 'FROM alpine:3.21\nRUN apk add --no-cache python3 && rm -rf /usr/lib/python3.12\n' > /tmp/Dockerfile.be
docker build -q -t prova:be -f /tmp/Dockerfile.be /tmp
docker image ls prova --format "table {{.Tag}}\t{{.Size}}"48 MB de diferència per moure un rm d'una línia a una altra. El fitxer esborrat "existeix" igualment dins de la imatge mal, invisible però descarregable.
D'aquí les tres regles d'or de RUN:
- Agrupa les operacions relacionades en un sol
RUNamb&&i\. - Neteja a la mateixa capa on embrutes. Memòries cau de gestors de paquets, fitxers temporals, codi font que ja has compilat.
- Però no ho agrupis tot. Si fiques en un únic
RUNla instal·lació de dependències i l'arrencada de l'aplicació, perds granularitat de memòria cau. L'equilibri: unRUNper propòsit.
Neteja segons el gestor de paquets
| Base | Comanda | Nota |
|---|---|---|
| Alpine | apk add --no-cache paquet |
--no-cache evita escriure l'índex: no cal cap rm posterior |
| Debian/Ubuntu | apt-get update && apt-get install -y --no-install-recommends paquet && rm -rf /var/lib/apt/lists/* |
El rm és imprescindible i ha d'anar al mateix RUN |
| Node | npm ci --omit=dev && npm cache clean --force |
La memòria cau d'npm pot ocupar centenars de MB |
Un avís sobre Debian: apt-get update i apt-get install han d'anar sempre al mateix RUN. Si els separes, la memòria cau pot reutilitzar un update de fa setmanes mentre executa un install nou, i acabaràs instal·lant versions que ja no són als repositoris. És el clàssic error conegut com a cache busting d'apt.
RUN a Aurora Libros
Desmenuçat:
npm cien lloc denpm install.cisignifica clean install: esborranode_modulessi existeix i instal·la exactament les versions fixades apackage-lock.json, sense resoldre rangs. Si el lock i elpackage.jsonno concorden, falla en lloc d'improvisar. Això és justament el que vols en una imatge: dues builds del mateix commit produeixen els mateixos bytes.npm install, en canvi, pot resoldre^4.21.2com a4.21.2avui i com a4.22.0el mes que ve, produint imatges diferents del mateix codi.--omit=devexclou lesdevDependencies. A Aurora Libros avui no n'hi ha cap, però així que entrin linters o frameworks de test, aquesta opció evitarà que viatgin a producció. Substitueix l'antiga--production, ara desaconsellada.npm cache clean --forceesborra la memòria cau que npm deixa a~/.npm, que pot voltar els 50 MB. Va al mateixRUNper tot l'explicat més amunt: en unRUNa part no estalviaria ni un sol byte.
ENV: configuració que sobreviu a la construcció
ENV: configuració que sobreviu a la construccióENV defineix variables d'entorn que existeixen durant la resta de la construcció i també dins del contenidor en execució. Aquesta doble vida és el seu tret distintiu, i és el que la diferencia d'ARG (lliçó 02-04).
Sintaxi:
ENV CLAU=valor
ENV CLAU1=valor1 CLAU2=valor2 # Diverses en una instrucció, una sola capa
ENV CLAU valor # Forma antiga, sense '=': desaconselladaFes servir sempre la forma amb =. L'antiga és ambigua amb valors que contenen espais i està en desús.
Les variables definides es poden fer servir en instruccions posteriors:
I persisteixen en execució. Comprova-ho amb la imatge que ja tens:
L'important és que són valors per defecte sobreescrivibles en arrencar el contenidor:
Aquell -e té prioritat sobre l'ENV de la imatge. És exactament el mecanisme que fa que el server.js de la lliçó 01-07 —que llegeix tota la seva configuració de process.env— serveixi sense canvis al teu portàtil i en producció.
Què posar i què NO posar en un ENV
| Variable | A l'ENV del Dockerfile? |
Per què |
|---|---|---|
NODE_ENV=production |
Sí | No és secret i és el valor correcte per defecte per a la imatge |
PORT=3000 |
Sí | Valor per defecte sensat, sobreescrivible amb -e |
DB_HOST |
No | Depèn de l'entorn; s'injecta en executar |
DB_USER |
No | Depèn de l'entorn |
DB_PASSWORD |
MAI | És un secret. Queda gravat en una capa i visible amb docker image history |
L'última fila és la regla que arrossegues des de la lliçó 01-07 i que ara pots demostrar. Si algú escrivís ENV DB_PASSWORD=superSecreta2026, qualsevol persona amb accés a la imatge la veuria:
["PATH=/usr/local/sbin:...","NODE_VERSION=22.14.0","YARN_VERSION=1.22.22","NODE_ENV=production","PORT=3000"]Aquí hi ha tot, en clar, sense ni tan sols haver d'arrencar el contenidor. I publicant la imatge al repositori públic auroralibros/aurora-api, a Internet.
NODE_ENV=production mereix un comentari perquè a Node no és decoratiu: Express desactiva vistes de depuració i posa les plantilles a la memòria cau, moltes biblioteques redueixen el registre i npm install ometria les devDependencies. És un canvi de comportament real, no una etiqueta.
EXPOSE: documentació, no publicació
EXPOSE: documentació, no publicacióAquí hi ha el parany que atrapa tothom. Llegint "expose" qualsevol entén "obre aquest port a l'exterior". EXPOSE no obre res, no publica res i no canvia el comportament de la xarxa. És exclusivament documentació en forma de metadada.
El que sí que fa:
- Deixa constància a les metadades de la imatge de quin port fa servir el servei, perquè qui l'executi ho sàpiga sense llegir el codi.
- Apareix a
docker image inspecti a la columna PORTS dedocker ps. - Habilita l'opció
docker run -P(majúscula), que publica tots els ports declarats ambEXPOSEen ports aleatoris alts de l'amfitrió. - Docker Compose i alguns orquestradors el llegeixen com a pista.
El que no fa: publicar el port. Això continua sent feina exclusiva de -p a docker run, tal com vas aprendre a la lliçó 01-06.
Demostra-ho. La teva imatge 0.1.0 té EXPOSE 3000. Arrenca-la sense -p:
docker run -d --name prova-expose auroralibros/aurora-api:0.1.0
docker ps --filter name=prova-expose --format "table {{.Names}}\t{{.Ports}}"Fixa't en la columna PORTS: hi diu 3000/tcp, sense cap fletxa ->. Això significa "el contenidor declara aquest port", no "està publicat". Comprova-ho:
Ara amb -P majúscula, que sí que publica el que està declarat:
docker rm -f prova-expose
docker run -d --name prova-expose -P auroralibros/aurora-api:0.1.0
docker ps --filter name=prova-expose --format "table {{.Names}}\t{{.Ports}}"Ara sí que hi ha fletxa: el port 3000 del contenidor està publicat al 32768 de l'amfitrió, triat a l'atzar. Neteja:
Aleshores, per què molestar-se a posar EXPOSE? Per tres raons sòlides:
- Documenta la interfície de la imatge. Qui la rebi sap quin port mapar sense llegir
server.js. - És el contracte amb l'orquestrador. Compose (mòdul 4), Swarm i Kubernetes (mòdul 6) el fan servir com a referència.
- Costa zero. És una metadada: no afegeix ni un sol byte a la imatge.
Sintaxi completa:
EXPOSE 3000 # TCP per defecte
EXPOSE 3000/tcp # Explícit
EXPOSE 53/udp # UDP
EXPOSE 3000 9229 # Diversos ports
CMD: quin procés arrenca el contenidor
CMD: quin procés arrenca el contenidorCMD defineix la comanda per defecte que s'executa en arrencar un contenidor a partir de la imatge. No s'executa durant la construcció: només es desa com a metadada.
I hi ha una regla que governa tota la resta, ja vista a la lliçó 01-06: el contenidor viu mentre visqui el seu procés principal. Quan el procés del CMD acaba, el contenidor s'atura. Per això CMD ["node", "server.js"] manté el contenidor viu (el servidor no acaba) i CMD ["echo", "hola"] produeix un contenidor que mor a l'instant.
Les dues formes, i per què importen de debò
# Forma exec (JSON) — LA CORRECTA
CMD ["node", "server.js"]
# Forma shell — problemàtica
CMD node server.jsSemblen equivalents, i en condicions normals ho són. La diferència apareix en aturar el contenidor, i és prou important com per veure-la amb detall.
Amb la forma exec, Docker executa node server.js directament. El procés node és el PID 1 dins del contenidor.
Amb la forma shell, Docker executa /bin/sh -c "node server.js". El PID 1 és sh, i node és un procés fill:
flowchart LR
subgraph EXEC["Forma exec: CMD [node, server.js]"]
E1["PID 1: node server.js"]
end
subgraph SHELL["Forma shell: CMD node server.js"]
S1["PID 1: /bin/sh -c"] --> S2["PID 7: node server.js"]
end
SIG1["docker stop<br/>SIGTERM"] --> E1
SIG2["docker stop<br/>SIGTERM"] --> S1
E1 -.->|"tanca connexions<br/>i acaba netament"| OK["Aturada en ~0,2 s ✅"]
S1 -.->|"sh no reenvia el senyal"| KO["10 s d'espera<br/>i SIGKILL ❌"]
Quan executes docker stop, Docker envia SIGTERM al PID 1 i espera 10 segons abans d'enviar SIGKILL. Amb la forma exec, node rep el SIGTERM i pot tancar connexions, buidar memòries intermèdies i acabar. Amb la forma shell, el SIGTERM el rep sh, que no el reenvia als seus fills: node no se n'assabenta, continua treballant, i als 10 segons el mata un SIGKILL que no admet neteja. Peticions tallades per la meitat i transaccions a mig fer.
Mesura-ho:
# Amb forma exec (la teva imatge actual)
docker run -d --name t-exec auroralibros/aurora-api:0.1.0
time docker stop t-exec# Amb forma shell
printf 'FROM auroralibros/aurora-api:0.1.0\nCMD node server.js\n' > /tmp/Dockerfile.shell
docker build -q -t aurora-shell -f /tmp/Dockerfile.shell /tmp
docker run -d --name t-shell aurora-shell
time docker stop t-shell0,3 segons davant de 10,2. Deu segons de diferència per uns claudàtors, multiplicats per cada contenidor a cada desplegament. En un desplegament continu amb vint rèpliques (mòdul 6), aquesta diferència és la que separa un rollout net d'un amb errors per als usuaris.
Neteja:
Regla: fes servir sempre la forma exec, amb claudàtors i cometes dobles. És JSON: les cometes simples no valen.
Altres propietats de CMD
- Només l'últim compta. Si escrius diversos
CMD, els anteriors es descarten sense avís. - Se sobreescriu des de la línia de comandes. Tot el que posis després del nom de la imatge reemplaça el
CMD:
No ha arrencat el servidor: has substituït el CMD per node --version. Aquesta flexibilitat és útil per depurar (docker run --rm -it lasevaimatge sh et dona un intèrpret d'ordres dins de la imatge) i és la base del patró ENTRYPOINT + CMD de la lliçó 02-04.
- Si la imatge base té un
ENTRYPOINT, elCMDes converteix en els seus arguments. És justament el tema de 02-04. - No facis servir
CMDper a diversos processos.CMD ["sh", "-c", "node server.js & nginx"]és un antipatró: un contenidor, un procés. Aurora Libros té quatre serveis perquè tindrà quatre contenidors.
- El Dockerfile definitiu d'
aurora-api
aurora-apiJa pots justificar cada línia. Aquest és el Dockerfile final del mòdul, que substitueix el mínim de la lliçó 02-02. Desa'l a ~/aurora-libros/api/Dockerfile:
# syntax=docker/dockerfile:1
# -----------------------------------------------------------------------------
# Imatge d'aurora-api · API REST del catàleg d'Aurora Libros S.L.
# Construir amb: docker build -t auroralibros/aurora-api:1.0.0 .
# -----------------------------------------------------------------------------
# 1. Imatge base oficial: Node 22 (exigit per engines) sobre Alpine (~142 MB
# davant dels ~1,1 GB de node:22). Etiqueta amb la versió major fixada per
# rebre pedaços sense salts de versió inesperats.
FROM node:22-alpine
# 2. Directori de treball. Es crea si no existeix i persisteix en execució:
# és on aterra 'docker exec -it aurora-api sh'.
WORKDIR /app
# 3. Només els manifests de dependències. En anar ABANS del codi, la capa del
# npm ci es reutilitza mentre package.json i el lock no canviïn.
# El comodí cobreix package.json i package-lock.json en una sola instrucció.
COPY package*.json ./
# 4. Instal·lació reproduïble:
# npm ci -> versions EXACTES del lock; falla si no concorda
# --omit=dev -> fora les dependències de desenvolupament
# npm cache clean -> allibera ~50 MB de memòria cau A LA MATEIXA CAPA, que és
# l'única manera que l'esborrat estalviï espai de debò
RUN npm ci --omit=dev && npm cache clean --force
# 5. Ara el codi de l'aplicació, el més volàtil. El .dockerignore ja ha deixat
# fora node_modules/, .git/ i .env.
COPY . .
# 6. Configuració per defecte, sobreescrivible amb -e en executar.
# NODE_ENV=production activa optimitzacions reals a Express.
# AQUÍ NO HI VA CAP SECRET: quedaria gravat a les metadades de la imatge.
ENV NODE_ENV=production \
PORT=3000
# 7. Documenta que el servei escolta al 3000. NO publica el port:
# per a això continua calent -p 3000:3000 a docker run.
EXPOSE 3000
# 8. Procés principal, en forma exec perquè node sigui el PID 1 i rebi
# SIGTERM directament: aturada en 0,3 s en lloc de 10 s.
CMD ["node", "server.js"]Construeix la versió 1.0.0, que és la que acompanya el version del package.json:
[+] Building 11.9s (11/11) FINISHED
=> [1/5] FROM docker.io/library/node:22-alpine@sha256:9f2c... 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 47.83kB 0.0s
=> [2/5] WORKDIR /app 0.1s
=> [3/5] COPY package*.json ./ 0.0s
=> [4/5] RUN npm ci --omit=dev && npm cache clean --force 8.9s
=> [5/5] COPY . . 0.1s
=> exporting to image 0.6s
=> => naming to docker.io/auroralibros/aurora-api:1.0.0 0.0sLa demostració de l'encert de memòria cau
Canvia el codi i reconstrueix, que és el que faràs cinquanta vegades al dia:
[+] Building 1.2s (11/11) FINISHED
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package*.json ./ 0.0s
=> CACHED [4/5] RUN npm ci --omit=dev && npm cache clean --force 0.0s
=> [5/5] COPY . . 0.1s
=> exporting to image 0.5s1,2 segons, amb el npm ci intacte a la memòria cau. L'ordre de les instruccions —dependències a dalt, codi a baix— fa exactament el que es va dissenyar.
Verifica la imatge acabada:
docker image ls auroralibros/aurora-api
docker image inspect auroralibros/aurora-api:1.0.0 \
--format 'WorkingDir: {{.Config.WorkingDir}}
Cmd: {{json .Config.Cmd}}
Env: {{json .Config.Env}}
Ports: {{json .Config.ExposedPorts}}'REPOSITORY TAG IMAGE ID CREATED SIZE
auroralibros/aurora-api 1.0.0 8c1e4a7f2b9d 4 seconds ago 167MB
auroralibros/aurora-api 0.1.0 6b4d2f8e1a3c 22 minutes ago 167MB
WorkingDir: /app
Cmd: ["node","server.js"]
Env: ["PATH=...","NODE_VERSION=22.14.0","YARN_VERSION=1.22.22","NODE_ENV=production","PORT=3000"]
Ports: {"3000/tcp":{}}Totes les metadades que has anat declarant hi són, llegibles. I cap credencial entre elles, com marca la regla del projecte. Última comprovació funcional:
docker run -d --name aurora-api-v1 -p 3000:3000 auroralibros/aurora-api:1.0.0
curl -s http://localhost:3000/salut
docker rm -f aurora-api-v1{"servei":"aurora-api","version":"1.0.0","db":"ko","cache":"ko",
"errorDb":"connect ECONNREFUSED 127.0.0.1:5432","errorCache":"connect ECONNREFUSED 127.0.0.1:6379"}Igual que a 02-02: el servei viu i respon; les dependències continuen sense existir, i això és el mòdul 3.
Errors Habituals i Consells
RUN cd /appesperant que persisteixi. CadaRUNcorre en un contenidor temporal diferent. Fes servirWORKDIR.CMDen forma shell. Deu segons extra a cada aturada i senyals que no arriben mai al teu procés. Fes servir sempreCMD ["executable", "arg"]amb cometes dobles: és JSON, no Python.- Creure que
EXPOSEpublica el port. No publica res.EXPOSEdocumenta;-ppublica. Si el teu servei "no respon" i adocker psno veus la fletxa->a PORTS, aquest és el problema. - Netejar en un
RUNdiferent del que embruta. L'esborrat no allibera res: la capa inferior conserva els fitxers. Encadena amb&&a la mateixa instrucció. apt-get updateiapt-get installenRUNseparats. La memòria cau reutilitza un índex vell i l'installfalla o instal·la versions obsoletes. Sempre junts.npm installen lloc denpm ci. Trenca la reproductibilitat: el mateix commit pot produir imatges diferents. I no oblidis--omit=dev.ADDper costum. El seu comportament depèn del tipus de fitxer i amaga descàrregues remotes sense verificar. Fes servirCOPYllevat que necessitis descomprimir un tarball local.- Secrets a
ENV. Queden a les metadades, visibles ambdocker image inspectsense ni tan sols arrencar el contenidor, i viatgen amb la imatge allà on vagi. Mai. COPY . .sense.dockerignore. Ja ho vas veure a 02-02: context inflat, memòria cau trencada i risc de filtrar un.env.- Consell: comenta el per què, no el què.
# Instal·la dependènciessobra: es veu.# El cache clean va aquí perquè alliberi espai de debòés el que agrairà qui ho llegeixi d'aquí a sis mesos. - Consell: llegeix Dockerfiles aliens. Els de les imatges oficials són a GitHub (lliçó 02-01) i són una escola excel·lent.
Exercicis
Exercici 1: demostra que la neteja ha d'anar a la mateixa capa
Construeix dues imatges basades en node:22-alpine que instal·lin el paquet git amb apk:
- Versió A:
apk add giten unRUNirm -rf /var/cache/apk/*en un altreRUNposterior. - Versió B: tot encadenat amb
&&en un únicRUN, fent servirapk add --no-cache.
Compara les mides amb docker image ls i fes servir docker image history per localitzar la capa culpable de la diferència. Explica el resultat en termes de capes i copy-on-write.
Exercici 2: mesura l'impacte de la forma del CMD
- Escriu dos Dockerfiles idèntics llevat del
CMD: un en forma exec i un altre en forma shell, tots dos arrencantnode server.jsdes de la imatge d'Aurora Libros. - Arrenca un contenidor de cadascun.
- Amb
docker exec <contenidor> ps -o pid,comm, comprova quin procés és el PID 1 en cada cas. - Cronometra
docker stopen tots dos ambtime. - Explica per què la diferència és d'exactament uns 10 segons i no d'un valor arbitrari, i quina opció de
docker stoppermetria canviar aquesta espera.
Exercici 3: audita i corregeix un Dockerfile
Aquest Dockerfile de l'API d'Aurora Libros té set problemes relacionats amb el que s'ha vist. Identifica'ls, explica la conseqüència de cadascun i escriu la versió corregida.
FROM node:latest
ADD . /app
RUN cd /app
RUN npm install
RUN npm cache clean --force
ENV DB_PASSWORD=aurora2026
ENV NODE_ENV production
EXPOSE 3000
CMD node /app/server.jsSolucions
Solució a l'exercici 1
mkdir -p /tmp/ex1 && cd /tmp/ex1
cat > Dockerfile.a <<'EOF'
FROM node:22-alpine
RUN apk add git
RUN rm -rf /var/cache/apk/*
EOF
cat > Dockerfile.b <<'EOF'
FROM node:22-alpine
RUN apk add --no-cache git
EOF
docker build -q -t ex1:a -f Dockerfile.a .
docker build -q -t ex1:b -f Dockerfile.b .
docker image ls ex1 --format "table {{.Tag}}\t{{.Size}}"Quatre megues de diferència (amb paquets més grans, la diferència arriba a desenes o centenars). Localitza la capa culpable:
SIZE CREATED BY
0B RUN /bin/sh -c rm -rf /var/cache/apk/* # buildkit
19.2MB RUN /bin/sh -c apk add git # buildkitLa lectura és concloent: la capa del rm -rf pesa 0 B. No ha alliberat res. Només ha escrit marcadors d'esborrat (whiteouts) que oculten els fitxers de la capa inferior, però aquella capa de 19,2 MB continua a la imatge i es descarrega sencera a cada docker pull.
A la versió B, --no-cache fa que apk no escrigui mai l'índex al disc, així que no hi ha res a esborrar. És l'aplicació directa del copy-on-write de la lliçó 01-05: una capa només pot afegir contingut o ocultar-lo, mai reduir la mida de les capes anteriors. D'aquí la regla: embruta i neteja a la mateixa instrucció.
Solució a l'exercici 2
cd /tmp/ex1
printf 'FROM auroralibros/aurora-api:1.0.0\nCMD ["node", "server.js"]\n' > Dockerfile.exec
printf 'FROM auroralibros/aurora-api:1.0.0\nCMD node server.js\n' > Dockerfile.shell
docker build -q -t cmd:exec -f Dockerfile.exec .
docker build -q -t cmd:shell -f Dockerfile.shell .
docker run -d --name c-exec cmd:exec
docker run -d --name c-shell cmd:shell3. Els processos:
Confirmat: en la forma exec node és el PID 1; en la forma shell el PID 1 és sh i node és el seu fill amb PID 7.
4. Els temps:
5. La diferència és de ~10 segons exactes perquè aquest és el valor del temporitzador de gràcia de docker stop: envia SIGTERM al PID 1, espera 10 segons i, si el contenidor continua viu, envia SIGKILL.
- En la forma exec,
noderep el SIGTERM. Encara que aquestserver.jsno instal·li un gestor explícit, el comportament per defecte de Node davant de SIGTERM és acabar immediatament: 0,3 s. - En la forma shell,
shrep el SIGTERM i no el reenvia als seus fills (un intèrpret d'ordres POSIX mínim no fa reenviament de senyals).nodeno se n'assabenta mai. S'esgoten els 10 segons i arriba el SIGKILL, que el procés no pot capturar: tancament abrupte, connexions tallades, sense oportunitat de buidar memòries intermèdies ni tancar el pool de PostgreSQL.
El termini es pot ajustar amb docker stop -t <segons>:
docker rm -f c-shell 2>/dev/null; docker run -d --name c-shell cmd:shell
time docker stop -t 2 c-shell # ~2 s: només escurça l'agonia, no arregla la causaAbaixar el termini no resol res: el procés continua sense rebre el senyal. La solució és la forma exec, o un ENTRYPOINT amb un script que faci servir exec "$@" (lliçó 02-04). Neteja:
Solució a l'exercici 3
Els set problemes:
| # | Línia | Problema | Conseqüència |
|---|---|---|---|
| 1 | FROM node:latest |
Etiqueta latest i variant completa |
Versió major impredictible (pot saltar a Node 24 sense avisar) i imatge de ~1,1 GB en lloc de ~142 MB |
| 2 | ADD . /app |
ADD on n'hi ha prou amb COPY |
Comportament depenent del tipus de fitxer; sense justificació aquí |
| 3 | RUN cd /app |
cd en un RUN propi |
No té cap efecte: el npm install següent s'executa a / i falla |
| 4 | RUN npm install |
Sense ci ni --omit=dev, i després de l'ADD . |
No reproduïble, inclou dependències de desenvolupament i la memòria cau s'invalida a cada canvi de codi |
| 5 | RUN npm cache clean en línia a part |
Neteja en una altra capa | No allibera ni un sol byte |
| 6 | ENV DB_PASSWORD=aurora2026 |
Secret a la imatge | Visible amb docker image inspect; catastròfic en un repositori públic |
| 7 | CMD node /app/server.js |
Forma shell | PID 1 = sh, SIGTERM no arriba a node, 10 s d'espera i SIGKILL a cada aturada |
Un vuitè detall menor: ENV NODE_ENV production fa servir la sintaxi antiga sense =, desaconsellada per ambigua.
Versió corregida:
# syntax=docker/dockerfile:1
# (1) Base oficial lleugera amb la versió major fixada
FROM node:22-alpine
# (3) WORKDIR en lloc de RUN cd: persisteix i crea el directori
WORKDIR /app
# (4) Dependències abans que codi, per preservar la memòria cau
COPY package*.json ./
# (4)(5) Instal·lació reproduïble i neteja A LA MATEIXA CAPA
RUN npm ci --omit=dev && npm cache clean --force
# (2) COPY en lloc d'ADD, i després de les dependències
COPY . .
# (6) Sense secrets. (8) Sintaxi amb '='
ENV NODE_ENV=production \
PORT=3000
EXPOSE 3000
# (7) Forma exec: node és PID 1 i rep SIGTERM
CMD ["node", "server.js"]La contrasenya s'injecta en executar, mai en construir:
docker run -d --name aurora-api \
-p 3000:3000 \
-e DB_HOST=aurora-db \
-e DB_PASSWORD=aurora2026 \
auroralibros/aurora-api:1.0.0I al mòdul 4 ni tan sols anirà a la línia de comandes, sinó en un fitxer .env fora del control de versions (lliçó 04-05) o en un secret gestionat (lliçó 05-03).
Conclusió
Ja domines el llenguatge del Dockerfile. Saps que la directiva # syntax=docker/dockerfile:1 et dona l'intèrpret més recent sense actualitzar Docker, i que de les vuit instruccions bàsiques només COPY, ADD i RUN engreixen la imatge: les altres escriuen metadades que no pesen res. FROM fixa el punt de partida i el compromís entre reproductibilitat i pedaços automàtics. WORKDIR substitueix un RUN cd que no funcionaria mai, perquè cada RUN viu en un contenidor temporal diferent, i a més persisteix en execució. COPY resol les seves rutes contra el context i copia el contingut dels directoris; ADD hi afegeix descompressió i descàrregues amb un comportament variable que la fa pitjor opció llevat que sigui per a tarballs locals.
Amb RUN has vist la regla més rendible de totes: neteja a la mateixa capa on embrutes, perquè una capa només pot afegir o ocultar, mai aprimar les anteriors —48 MB de diferència a la demostració amb python3—. Amb ENV has distingit el que és un valor per defecte legítim (NODE_ENV, PORT) del que mai no ha d'entrar en una imatge (DB_PASSWORD), i ho has comprovat llegint les metadades amb docker image inspect. Amb EXPOSE has desmuntat el parany del nom: documenta, no publica, i la prova és l'absència de fletxa -> a docker ps. I amb CMD has mesurat en carn pròpia que la forma exec i la forma shell no són estils alternatius: són 0,3 segons davant de 10,2 en aturar el contenidor, perquè el PID 1 és node o és sh, i sh no reenvia SIGTERM.
El resultat és auroralibros/aurora-api:1.0.0: 167 MB, comentada línia a línia, amb cada decisió justificada, que es reconstrueix en 1,2 segons després d'un canvi de codi i que no porta ni un sol secret a dins. És un Dockerfile que funciona.
A la lliçó següent, Instruccions Avançades del Dockerfile, el convertiràs en un de professional. Aprendràs a parametritzar la versió base amb ARG i a distingir-lo d'ENV, a separar l'executable fix dels seus arguments amb ENTRYPOINT i CMD combinats, a deixar d'executar l'API com a root amb USER, a descriure la imatge amb etiquetes OCI mitjançant LABEL, i a fer que Docker vigili pel seu compte l'endpoint /salut amb HEALTHCHECK perquè docker ps mostri healthy o unhealthy. Aquest és el salt entre una imatge que arrenca i una imatge que pots posar en producció sense envermellir.
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
