A la lliçó anterior vas aprendre d'on vénen les imatges: un registre, un repositori, una etiqueta que apunta a un digest. I vas reservar l'adreça on viurà la teva, auroralibros/aurora-api. Ara toca fabricar-la. Aquesta lliçó tracta del procés de construcció en si: què fa docker build des que prems Enter, què és aquell punt final que tothom copia sense saber què significa, per què una carpeta amb node_modules a dins pot convertir una build de dos segons en una de dos minuts, i com la memòria cau de capes de BuildKit premia o castiga l'ordre en què escrius les instruccions. Encara no estudiaràs el Dockerfile instrucció per instrucció —això és la lliçó següent—, sinó la màquina que l'interpreta. En acabar tindràs auroralibros/aurora-api:0.1.0 construïda, corrent en un contenidor i responent a un curl.

Contingut

  1. Què fa docker build realment
  2. Anatomia de la comanda i el punt final
  3. El context de construcció
  4. El fitxer .dockerignore
  5. Dockerfiles amb un altre nom o ruta: l'opció -f
  6. La memòria cau de capes de BuildKit
  7. Dependències abans que codi: ineficient davant d'eficient
  8. --no-cache i --pull: quan desconfiar de la memòria cau
  9. Llegir la sortida de BuildKit
  10. De zero a imatge: auroralibros/aurora-api:0.1.0

  1. Què fa docker build realment

Recorda l'arquitectura client-servidor de la lliçó 01-03: el client docker no fa la feina, la demana al dimoni. La construcció d'imatges segueix aquest mateix esquema, amb un actor més. Des de Docker Engine 23, el constructor per defecte és BuildKit, un motor de construcció independent i molt més capaç que el clàssic.

sequenceDiagram
    participant U as Tu (terminal)
    participant C as Client docker
    participant D as dockerd
    participant B as BuildKit
    participant R as Registre

    U->>C: docker build -t aurora-api:0.1.0 .
    C->>C: Llegeix el Dockerfile i el .dockerignore
    C->>D: Envia el context (fitxers no ignorats)
    D->>B: Sol·licita la construcció
    B->>B: Analitza el Dockerfile i calcula el graf de passos
    B->>R: Tinc node:22-alpine? Si no, la descarrega
    R-->>B: Capes de la imatge base
    loop Per cada instrucció
        B->>B: Hi ha memòria cau vàlida per a aquest pas?
        alt Hi ha memòria cau
            B-->>B: CACHED (reutilitza la capa)
        else No hi ha memòria cau
            B->>B: Executa el pas i crea una capa nova
        end
    end
    B->>D: Exporta les capes i el manifest
    D->>D: Desa la imatge i li posa l'etiqueta
    D-->>C: Build completada
    C-->>U: naming to docker.io/auroralibros/aurora-api:0.1.0

Tres conseqüències pràctiques d'aquest diagrama que convé interioritzar abans de continuar:

  1. La build no passa al teu directori de treball. Passa al constructor, que pot estar en una altra màquina. Per això cal enviar-li els fitxers: és el context de l'apartat 3.
  2. BuildKit construeix un graf, no una llista. No executa les instruccions cegament de dalt a baix: calcula quins passos depenen de quins i se salta els que ja té resolts. D'aquí ve la potència de la memòria cau.
  3. La imatge resultant es queda al teu magatzem local. docker build no publica res a cap registre; això és docker push, i arriba a la lliçó 02-06.

docker build i docker buildx build

Et trobaràs les dues formes i convé aclarir-les ja:

Comanda Què és
docker build La comanda clàssica. Des d'Engine 23 delega en BuildKit de manera transparent
docker buildx build La interfície completa de BuildKit, amb opcions addicionals (multiplataforma, exportadors, constructors remots)

Per a tot el d'aquest mòdul són equivalents: docker build és més curt i és el que faràs servir. Les capacitats exclusives de buildx —construir per a diverses arquitectures alhora, muntatges de memòria cau, secrets de build— es veuen a la lliçó 05-05. Pots comprovar quin constructor estàs fent servir:

docker buildx ls
NAME/NODE       DRIVER/ENDPOINT   STATUS    BUILDKIT   PLATFORMS
default*        docker
 \_ default      \_ default       running   v0.19.0    linux/amd64, linux/386

L'asterisc marca el constructor actiu. BUILDKIT v0.19.0 confirma que BuildKit està en marxa; si aquella columna estigués buida, estaries amb el constructor antic i no veuries res del que es descriu en aquesta lliçó.

  1. Anatomia de la comanda i el punt final

La forma canònica és:

docker build -t auroralibros/aurora-api:0.1.0 .

Desmenuçada:

Part Què és Detall
docker build La comanda Construeix una imatge
-t auroralibros/aurora-api:0.1.0 --tag: nom i etiqueta de la imatge resultant Es pot repetir per donar diversos noms a la mateixa build
. El context de construcció El directori el contingut del qual s'envia al constructor

El punt final és la part que més confusió genera. No significa "construeix aquí" ni "fes servir el Dockerfile d'aquest directori", encara que per defecte produeixi tots dos efectes. Significa: el context de construcció és el directori actual. És a dir, "envia al constructor tot el que hi ha a . (menys allò ignorat), perquè les instruccions COPY aniran a buscar els seus fitxers allà dins".

Es veu millor amb variants:

# Context = directori actual; Dockerfile = ./Dockerfile
docker build -t aurora-api:0.1.0 .

# Context = ./api ; Dockerfile = ./api/Dockerfile
docker build -t aurora-api:0.1.0 ./api

# Context = directori pare; Dockerfile = ../Dockerfile
docker build -t aurora-api:0.1.0 ..

# Context = un repositori Git remot, clonat pel constructor
docker build -t aurora-api:0.1.0 https://github.com/auroralibros/aurora-libros.git#main:api

L'última forma és reveladora: el context ni tan sols ha de ser a la teva màquina. BuildKit pot clonar un repositori i construir des d'allà. Això demostra que el context és un concepte abstracte ("el conjunt de fitxers disponibles per a la build"), no "la carpeta on ets".

Altres opcions de docker build que faràs servir en aquest mòdul:

Opció Per a què
-t, --tag Nom i etiqueta; repetible
-f, --file Ruta a un Dockerfile amb un altre nom o ubicació (apartat 5)
--no-cache Ignora la memòria cau del tot (apartat 8)
--pull Força a descarregar la versió més recent de la imatge base (apartat 8)
--progress=plain Sortida completa, sense la interfície dinàmica (apartat 9)
--build-arg Passa un argument de construcció (lliçó 02-04)
--target Construeix fins a una etapa concreta (multietapa, lliçó 05-04)

  1. El context de construcció

Abans d'executar cap instrucció, el client empaqueta el context i l'envia al constructor. Això és important perquè tot el que hi ha en aquell directori viatja, tant si el fas servir en algun COPY com si no.

Comprova la mida del teu context abans de construir. És un hàbit que val el seu pes en or:

cd ~/aurora-libros/api
du -sh .
23M     .

Vint-i-tres megues per a un server.js de 100 línies i un package.json. D'on surten?

du -sh * .[!.]* 2>/dev/null | sort -h
4,0K    package.json
8,0K    server.js
44K     package-lock.json
23M     node_modules

Aquí està: node_modules, generat quan vas fer npm install a la lliçó 01-07. I aquests 23 MB no aporten res a la imatge, perquè les dependències s'instal·laran a dins seu amb npm ci.

Conseqüències d'un context gran:

  • La build és més lenta, i el retard és abans de la primera instrucció: el client ha de llegir, comprimir i transmetre tots aquests fitxers. En un projecte amb .git de 500 MB, això són uns quants segons a cada build.
  • Risc de filtració. Un COPY . . descurat fica dins la imatge tot el que hi hagi al context, inclosos fitxers .env, claus SSH o bolcats de base de dades. És un incident de seguretat real, no teòric.
  • Trenca la memòria cau. Com veuràs a l'apartat 6, la memòria cau d'un COPY . . s'invalida si canvia qualsevol fitxer del context. Amb node_modules a dins, un npm install local invalida la build sencera.

L'error clàssic: COPY fora del context

Aquest error el cometràs, així que millor entendre'l ara. Suposa que des de ~/aurora-libros/api intentes copiar l'init.sql que és a ~/aurora-libros/db/:

FROM node:22-alpine
WORKDIR /app
COPY ../db/init.sql ./
cd ~/aurora-libros/api
docker build -t prova .
ERROR: failed to solve: failed to compute cache key: failed to calculate checksum of
ref ...: "/db/init.sql": not found

El missatge despista perquè parla de "not found", però el fitxer existeix perfectament al teu disc. El que passa és que no existeix al context: com que el context és . (o sigui, api/), el constructor només ha rebut el que hi ha dins d'api/, i ../db/ en queda fora. Les rutes d'origen de COPY són sempre relatives a l'arrel del context i mai no en poden sortir. És una restricció de seguretat deliberada: si no fos així, qualsevol Dockerfile podria copiar /etc/shadow o ~/.ssh/id_rsa de la màquina que construeix.

Les dues solucions legítimes:

# a) Ampliar el context a l'arrel del projecte i apuntar al Dockerfile amb -f
cd ~/aurora-libros
docker build -t prova -f api/Dockerfile .

# b) Deixar el context a api/ i copiar el fitxer allà (si de debò pertany a l'API)

Per a Aurora Libros triarem l'opció de context reduït: la imatge de l'API no necessita init.sql, que és cosa del contenidor de PostgreSQL (mòdul 3). Cada imatge porta només el que és seu.

  1. El fitxer .dockerignore

A la lliçó 01-07 va quedar anunciat: la imatge de l'API tindrà un .dockerignore que exclogui node_modules/, .git/ i .env. És el moment d'escriure'l.

.dockerignore és un fitxer de text a l'arrel del context que li diu al client què no ha d'enviar al constructor. La seva sintaxi recorda la de .gitignore, però té regles pròpies:

Patró Què exclou
node_modules Qualsevol fitxer o carpeta amb aquest nom a l'arrel del context
**/node_modules Aquest nom a qualsevol nivell de profunditat
*.log Tots els fitxers amb extensió .log a l'arrel
**/*.log Tots els .log a qualsevol subdirectori
.git El directori de Git complet
temp? temp1, tempA… (? = un caràcter qualsevol)
!important.log Excepció: torna a incloure aquest fitxer encara que un patró anterior l'exclogués
# comentari Línia ignorada

Dues diferències amb .gitignore que causen sorpreses:

  • node_modules no és recursiu per si sol, a diferència de Git. Si tens subprojectes amb les seves pròpies dependències, necessites **/node_modules.
  • L'ordre importa quan fas servir !: l'última regla que coincideix és la que mana.

Crea ~/aurora-libros/api/.dockerignore:

# Dependències: s'instal·len dins de la imatge amb npm ci
node_modules
**/node_modules

# Control de versions
.git
.gitignore

# Secrets i configuració local: MAI dins d'una imatge
.env
.env.*

# Registres i temporals
*.log
npm-debug.log*
.npm
.cache
tmp/

# Metadades del sistema operatiu i de l'editor
.DS_Store
Thumbs.db
.vscode
.idea

# El Dockerfile mateix i els seus auxiliars: el constructor ja els té
Dockerfile
Dockerfile.*
.dockerignore

# Proves i documentació: no s'executen en producció
test/
*.test.js
README.md
NOTES-ONBOARDING.md

Justificació de les entrades menys evidents:

  • node_modules: la raó principal. A més de la mida, copiar el node_modules de la teva màquina pot ficar-hi binaris compilats per al teu sistema operatiu i arquitectura que no funcionin dins d'una imatge Alpine (és exactament l'avís de l'exercici 3 de 01-07).
  • .env: la regla innegociable del projecte. Les credencials no entren a la imatge. Un .dockerignore que l'exclogui és la primera línia de defensa davant d'un COPY . . descurat.
  • Dockerfile: no cal dins de la imatge. Excloure'l té un efecte secundari molt útil: editar el Dockerfile ja no invalida la memòria cau del COPY . ..
  • README.md, test/: no s'executen en producció, i cada byte que no hi entra és un byte que no es descarrega a cada desplegament.

Mesurar l'abans i el després

La manera neta de comprovar-ne l'efecte és amb --progress=plain, que mostra la línia de transferència del context. Primer, sense .dockerignore:

cd ~/aurora-libros/api
mv .dockerignore .dockerignore.off
docker build --no-cache --progress=plain -t mesura:sense -f Dockerfile . 2>&1 | grep -i "transferring context"
#2 [internal] load build context
#2 transferring context: 23.41MB 1.8s done

Ara amb ell:

mv .dockerignore.off .dockerignore
docker build --no-cache --progress=plain -t mesura:amb -f Dockerfile . 2>&1 | grep -i "transferring context"
#2 [internal] load build context
#2 transferring context: 47.83kB 0.0s done

De 23,41 MB a 47,83 kB: una reducció de més del 99 %, i el temps de transferència baixa d'1,8 segons a pràcticament zero. En un projecte real amb .git voluminós, artefactes de compilació i captures de pantalla, la diferència es mesura en centenars de megues i desenes de segons a cada build.

  1. Dockerfiles amb un altre nom o ruta: l'opció -f

Per defecte, docker build busca un fitxer anomenat exactament Dockerfile a l'arrel del context. -f trenca aquest acoblament:

# Un Dockerfile amb un altre nom, al mateix directori
docker build -t aurora-api:dev -f Dockerfile.dev .

# Context a l'arrel del projecte, Dockerfile dins d'api/
cd ~/aurora-libros
docker build -t auroralibros/aurora-api:0.1.0 -f api/Dockerfile ./api

# Dockerfiles centralitzats en una carpeta a part
docker build -t aurora-web:0.1.0 -f docker/web.Dockerfile ./web

Punts clau:

  • -f i el context són independents. Pots tenir el Dockerfile a qualsevol lloc i el context en un altre. El que no canvia mai és que les rutes de COPY es resolen contra el context, no contra la ubicació del Dockerfile. És la confusió número u amb -f.
  • La ruta de -f sí que és relativa al teu directori actual, no al context.
  • El cas d'ús més comú és tenir variants: Dockerfile per a producció i Dockerfile.dev amb eines de desenvolupament. Aurora Libros farà servir un únic Dockerfile; les diferències entre entorns es resoldran amb Compose (lliçó 04-06), que és un enfocament més net.

  1. La memòria cau de capes de BuildKit

Aquí hi ha la diferència entre esperar noranta segons a cada canvi de codi o esperar-ne dos. Recorda de la lliçó 01-05 que una imatge és una pila de capes. Durant la construcció, cada instrucció del Dockerfile que modifica el sistema de fitxers produeix una capa, i BuildKit intenta reutilitzar les que ja té.

Com decideix BuildKit si reutilitza una capa

Per a cada instrucció, BuildKit calcula una clau de memòria cau a partir de:

  1. La clau de la capa anterior (per això l'ordre ho és tot).
  2. El text literal de la instrucció. Canviar un espai o un comentari dins d'un RUN la invalida.
  3. Per a COPY i ADD, a més, el checksum del contingut dels fitxers copiats (no la data de modificació: si toques un fitxer sense canviar-ne el contingut, la memòria cau aguanta).

Si aquesta clau existeix a la memòria cau local, marca el pas com a CACHED i no executa res. Si no existeix, executa el pas i tots els posteriors, sense excepció.

La invalidació en cascada

Aquesta és la propietat que governa el disseny de qualsevol Dockerfile:

flowchart TB
    A["FROM node:22-alpine<br/>capa base"] --> B["WORKDIR /app"]
    B --> C["COPY package*.json ./"]
    C --> D["RUN npm ci<br/>⏱ 45 s"]
    D --> E["COPY . .<br/>el teu codi"]
    E --> F["CMD [node, server.js]"]

    style C fill:#f4f0fa
    style D fill:#f4f0fa
    style E fill:#ffe0e0
    style F fill:#ffe0e0

Si canvies una línia de server.js, s'invalida el COPY . . (pas E) i, en cascada, tot el que ve després. Però els passos A–D continuen a la memòria cau, inclòs el npm ci de 45 segons. Resultat: la build triga un parell de segons.

Ara inverteix l'ordre —copia el codi abans d'instal·lar dependències— i el mateix canvi a server.js invalida el COPY . ., que ara és abans del npm ci. Conseqüència: el npm ci es torna a executar sencer. Quaranta-cinc segons, a cada canvi d'una coma.

D'aquí ve la regla d'or: ordena les instruccions de menys a més volàtil. El que gairebé mai no canvia (la imatge base, la instal·lació de paquets del sistema, les dependències) va a dalt; el que canvies cinquanta vegades al dia (el teu codi) va a baix.

  1. Dependències abans que codi: ineficient davant d'eficient

Ho mesurarem, perquè el número convenç més que l'explicació.

Versió INEFICIENT

FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
CMD ["node", "server.js"]

Sembla raonable: copio el projecte i després instal·lo. I funciona. El problema és que COPY . . inclou server.js, així que qualsevol canvi al codi invalida aquesta capa i, amb ella, el npm ci.

Versió EFICIENT

FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

La diferència és un COPY partit en dos. Primer es copien només els manifests de dependències, s'instal·len, i després es copia la resta del codi. Com que package.json i package-lock.json canvien poques vegades, el npm ci queda a la memòria cau gairebé sempre.

El mesurament

Desa cada versió al seu fitxer i cronometra el cicle realista: primera build en fred, i segona build després de tocar el codi.

cd ~/aurora-libros/api

# Primera build de cada variant (totes dues en fred)
time docker build --no-cache -q -t ineficient -f Dockerfile.ineficient . > /dev/null
time docker build --no-cache -q -t eficient   -f Dockerfile.eficient   . > /dev/null

# Simula un canvi de codi, que és el que passa cinquanta vegades al dia
echo "// ajust menor" >> server.js

# Reconstrueix totes dues, ara aprofitant la memòria cau
time docker build -q -t ineficient -f Dockerfile.ineficient . > /dev/null
time docker build -q -t eficient   -f Dockerfile.eficient   . > /dev/null

Resultats típics en una màquina de desenvolupament:

Escenari Ineficient Eficient
Primera build (en fred) 48,3 s 49,1 s
Després de canviar server.js 46,7 s 1,4 s
Després de canviar package.json 47,9 s 48,5 s

Llegeix la taula amb atenció, perquè conté els tres missatges de la lliçó:

  • En fred són iguals (l'eficient fins i tot una mica més lenta, per tenir un COPY més). La memòria cau no accelera la primera vegada.
  • Després d'un canvi de codi, la diferència és de 33×. Multiplica-ho per cinquanta builds al dia i per cada persona de l'equip d'Aurora Libros: són hores.
  • Després de canviar les dependències, tornen a igualar-se, i és el correcte: si package.json canvia, cal reinstal·lar. La memòria cau no menteix, està fent la seva feina.

Un matís sobre npm ci que en justifica l'ús davant de npm install, i que a la lliçó 02-03 es reprèn: npm ci esborra node_modules i instal·la exactament les versions fixades a package-lock.json, i falla si el lock i el package.json no concorden. npm install pot resoldre versions diferents segons quan s'executi, cosa que trenca la reproductibilitat. En una imatge, sempre npm ci.

  1. --no-cache i --pull: quan desconfiar de la memòria cau

La memòria cau és una optimització basada en una suposició: que la mateixa instrucció amb les mateixes entrades produeix el mateix resultat. De vegades aquesta suposició és falsa.

# Ignora tota la memòria cau: executa cada instrucció des de zero
docker build --no-cache -t auroralibros/aurora-api:0.1.0 .

# Comprova si hi ha una versió més recent de la imatge base i la descarrega
docker build --pull -t auroralibros/aurora-api:0.1.0 .

# Totes dues: la build més reproduïble possible
docker build --no-cache --pull -t auroralibros/aurora-api:0.1.0 .

Quan fer servir cadascuna:

Situació Opció Per què
Build de release o de CI --pull (i sovint --no-cache) Garanteix base actualitzada i absència d'estat heretat
apt-get install / apk add que porta paquets vells --no-cache La instrucció és idèntica, però els repositoris remots han canviat
RUN git clone o curl d'un recurs remot --no-cache Igual: la instrucció no canvia, el contingut remot sí
Sospites d'una memòria cau corrupta o d'un comportament inexplicable --no-cache Descarta la memòria cau com a variable del problema
Desenvolupament diari Cap Estaries llençant a les escombraries l'avantatge de l'apartat 7

El cas de --pull mereix una explicació a part, perquè és subtil. Si el teu Dockerfile diu FROM node:22-alpine i ja tens aquella etiqueta descarregada, BuildKit la fa servir sense comprovar si ha canviat al registre. Però 22-alpine és una etiqueta mòbil (lliçó 02-01): Node publica pedaços i aquella etiqueta passa a apuntar a un altre digest. Sense --pull, pots estar construint durant mesos sobre una base amb vulnerabilitats ja corregides aigües amunt. Per això --pull és pràcticament obligatori als pipelines de construcció (lliçó 06-02).

I un avís: --no-cache no esborra la memòria cau existent, només la ignora en aquella build. Per alliberar realment l'espai que ocupa hi ha una comanda específica, docker builder prune, que veuràs a la lliçó 02-05.

  1. Llegir la sortida de BuildKit

BuildKit imprimeix una interfície dinàmica que es reescriu al lloc. Aprendre a llegir-la és aprendre a diagnosticar builds.

[+] Building 12.4s (11/11) FINISHED                              docker:default
 => [internal] load build definition from Dockerfile                       0.0s
 => => transferring dockerfile: 421B                                       0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine          1.1s
 => [internal] load .dockerignore                                          0.0s
 => => transferring context: 583B                                          0.0s
 => [1/5] FROM docker.io/library/node:22-alpine@sha256:9f2c...             2.8s
 => => resolve docker.io/library/node:22-alpine@sha256:9f2c...             0.0s
 => => sha256:9f2c... 1.72kB / 1.72kB                                      0.0s
 => => extracting sha256:4a1b...                                           0.4s
 => [internal] load build context                                          0.0s
 => => transferring context: 47.83kB                                       0.0s
 => CACHED [2/5] WORKDIR /app                                              0.0s
 => [3/5] COPY package.json package-lock.json ./                           0.0s
 => [4/5] RUN npm ci --omit=dev && npm cache clean --force                 7.9s
 => [5/5] COPY . .                                                         0.0s
 => exporting to image                                                     0.5s
 => => exporting layers                                                    0.4s
 => => writing image sha256:6b4d...                                        0.0s
 => => naming to docker.io/auroralibros/aurora-api:0.1.0                   0.0s

Clau de lectura, línia a línia:

Element Significat
[+] Building 12.4s (11/11) FINISHED Temps total i passos completats / totals
[internal] … Passos interns: llegir el Dockerfile, el .dockerignore, resoldre metadades de la base
transferring context: 47.83kB La mida del teu context. És la línia que vigiles després d'escriure el .dockerignore
[1/5], [2/5] Passos del Dockerfile, numerats. Només compten les instruccions que generen capa
CACHED Pas reutilitzat de la memòria cau. Zero segons. El que vols veure
exporting layers Escriptura de les capes noves al magatzem local
writing image sha256:… L'ID de la imatge resultant
naming to … L'etiqueta que li has posat amb -t

El diagnòstic es fa mirant on deixa d'aparèixer CACHED. Aquest és el punt d'invalidació, i tot el que hi ha a sota s'ha reexecutat. Si el primer pas no cachejat és abans del que esperaves, tens un problema d'ordre d'instruccions o un fitxer volàtil colant-se en un COPY.

--progress=plain

La interfície dinàmica és còmoda però amaga la sortida de les comandes: no veus el que imprimeix npm ci. Quan alguna cosa falla, ho necessites veure tot:

docker build --progress=plain --no-cache -t auroralibros/aurora-api:0.1.0 . 2>&1 | tail -30
#8 [4/5] RUN npm ci --omit=dev && npm cache clean --force
#8 3.412 npm warn config production Use `--omit=dev` instead.
#8 7.108 added 112 packages, and audited 113 packages in 7s
#8 7.115 found 0 vulnerabilities
#8 7.883 npm warn using --force Recommended protections disabled.
#8 DONE 7.9s

El format és #<pas> <segons des de l'inici del pas> <línia de sortida>. Aquests temps relatius són or pur per saber quina part d'un RUN llarg és la lenta. Tres usos habituals de --progress=plain:

  • Veure per què falla un RUN (el missatge d'error complet de la comanda).
  • Mesurar la mida del context, com vas fer a l'apartat 4.
  • Desar un registre complet a CI, on la interfície dinàmica produeix brossa il·legible.

  1. De zero a imatge: auroralibros/aurora-api:0.1.0

És el moment d'ajuntar-ho tot. Construiràs la primera imatge d'Aurora Libros amb un Dockerfile mínim, presentat sencer. Aquí només s'explica per sobre què fa cada instrucció; el detall complet, les alternatives i les decisions fines són la lliçó 02-03.

Crea ~/aurora-libros/api/Dockerfile:

# syntax=docker/dockerfile:1

# Imatge base: Node.js 22 sobre Alpine Linux, tal com es va decidir a 01-07
FROM node:22-alpine

# Directori de treball dins de la imatge; la resta de rutes hi són relatives
WORKDIR /app

# Primer NOMÉS els manifests de dependències: així el npm ci queda a la memòria cau
COPY package.json package-lock.json ./

# Instal·lació reproduïble, sense dependències de desenvolupament, netejant la memòria cau d'npm
RUN npm ci --omit=dev && npm cache clean --force

# Ara sí, el codi de l'aplicació
COPY . .

# Configuració per defecte. Les credencials NO van aquí (regla del projecte)
ENV NODE_ENV=production
ENV PORT=3000

# Documenta que el servei escolta al 3000 (no publica res per si sol)
EXPOSE 3000

# Procés que s'executa en arrencar el contenidor
CMD ["node", "server.js"]

Una ullada ràpida a cada instrucció, sense entrar-hi en profunditat:

  • # syntax=docker/dockerfile:1: tria l'intèrpret de Dockerfile més recent de la sèrie 1.
  • FROM: de quina imatge es parteix.
  • WORKDIR: fixa el directori de treball dins de la imatge i el crea si no existeix.
  • COPY: porta fitxers del context a la imatge. Està partit en dos pel que s'ha vist a l'apartat 7.
  • RUN: executa una comanda durant la construcció i congela el resultat en una capa.
  • ENV: defineix variables d'entorn que persisteixen en execució.
  • EXPOSE: documentació. No publica el port; això continua sent cosa de -p (lliçó 01-06).
  • CMD: quin procés arrenca el contenidor.

Si et preguntes per què npm ci i no npm install, per què aquella forma de CMD amb claudàtors, o per què EXPOSE no fa el que el seu nom suggereix: tot això és exactament el contingut de la lliçó 02-03.

Construir

cd ~/aurora-libros/api
docker build -t auroralibros/aurora-api:0.1.0 .
[+] Building 13.7s (11/11) FINISHED                              docker:default
 => [internal] load build definition from Dockerfile                       0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine          1.2s
 => [internal] load .dockerignore                                          0.0s
 => [1/5] FROM docker.io/library/node:22-alpine@sha256:9f2c...             3.1s
 => [internal] load build context                                          0.0s
 => => transferring context: 47.83kB                                       0.0s
 => [2/5] WORKDIR /app                                                     0.1s
 => [3/5] COPY package.json package-lock.json ./                           0.0s
 => [4/5] RUN npm ci --omit=dev && npm cache clean --force                 8.4s
 => [5/5] COPY . .                                                         0.0s
 => exporting to image                                                     0.6s
 => => naming to docker.io/auroralibros/aurora-api:0.1.0                   0.0s

Comprova el resultat:

docker image ls auroralibros/aurora-api
REPOSITORY                 TAG       IMAGE ID       CREATED          SIZE
auroralibros/aurora-api    0.1.0     6b4d2f8e1a3c   9 seconds ago    167MB

167 MB, dels quals uns 142 són la base node:22-alpine que ja coneixies. La teva aplicació i les seves dependències aporten uns 25 MB.

Reconstruir i veure la memòria cau en acció

docker build -t auroralibros/aurora-api:0.1.0 .
[+] Building 0.4s (11/11) FINISHED
 => CACHED [2/5] WORKDIR /app                                              0.0s
 => CACHED [3/5] COPY package.json package-lock.json ./                    0.0s
 => CACHED [4/5] RUN npm ci --omit=dev && npm cache clean --force          0.0s
 => CACHED [5/5] COPY . .                                                  0.0s

Tot CACHED, 0,4 segons. De 13,7 s a 0,4 s sense canviar res.

Executar la imatge

docker run -d --name aurora-api-test -p 3000:3000 auroralibros/aurora-api:0.1.0
docker ps --filter name=aurora-api-test
CONTAINER ID   IMAGE                             STATUS         PORTS                    NAMES
d3f8a1c9b7e2   auroralibros/aurora-api:0.1.0     Up 4 seconds   0.0.0.0:3000->3000/tcp   aurora-api-test

Comandes ja conegudes de 01-06: -d en segon pla, --name per poder referir-t'hi, -p 3000:3000 per publicar el port. Mira els registres:

docker logs aurora-api-test
[aurora-api] escoltant al port 3000
[aurora-api] base de dades: localhost:5432/aurora_llibres
[aurora-api] memòria cau: localhost:6379

L'API ha arrencat dins d'un contenidor, sense que hagis instal·lat Node a la teva màquina. Prova-la:

curl -s http://localhost:3000/salut
{"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"}

L'endpoint respon (HTTP 503), que és el que volíem comprovar: el procés viu, Express escolta i la ruta funciona. Però db i cache estan en ko amb els mateixos ECONNREFUSED de la lliçó 01-07, i ara per una raó nova i molt instructiva: localhost dins del contenidor és el contenidor mateix, no la teva màquina. Allà dins no hi ha cap PostgreSQL ni cap Redis escoltant, i per això la connexió es rebutja. Encara que tinguessis els serveis corrent al teu portàtil, el contenidor no els veuria amb aquesta configuració.

I /llibres, que sí que necessita la base de dades:

curl -s http://localhost:3000/llibres
{"error":"No s'ha pogut obtenir el catàleg","detall":"connect ECONNREFUSED 127.0.0.1:5432"}

Exactament l'esperat. Aquesta fallada no és un error teu: és el problema següent del curs. Connectar contenidors entre si mitjançant xarxes pròpies, perquè DB_HOST=aurora-db signifiqui alguna cosa, és el contingut de la lliçó 03-05; i donar a PostgreSQL un volum on desar el catàleg, el de la 03-06.

Neteja abans de continuar:

docker stop aurora-api-test && docker rm aurora-api-test

La imatge es queda al teu magatzem local per a les lliçons següents. Un balanç d'allò aconseguit: dels quinze passos manuals de la lliçó 01-07, els passos 2, 3, 4 i 5 (descobrir la versió de Node, instal·lar nvm, instal·lar Node 22, executar npm install) han desaparegut. Qualsevol persona amb Docker pot executar la teva API sense instal·lar res relacionat amb Node.

Errors Habituals i Consells

  • Creure que el punt final significa "aquí hi ha el Dockerfile". Significa "aquest és el context". Són coses separades, i -f ho demostra. Així que ho interioritzes, la meitat dels errors de COPY desapareixen.
  • COPY ../alguna-cosa des de fora del context. No funcionarà mai, per disseny. Amplia el context i fes servir -f, o reorganitza els fitxers. Si et veus barallant-te amb això, gairebé sempre és senyal que la imatge intenta portar dins alguna cosa que no li correspon.
  • Oblidar el .dockerignore. Context lent, imatges inflades, memòria cau que s'invalida sola i risc real de filtrar un .env. Escriu-lo abans del primer docker build, no després.
  • COPY . . abans d'instal·lar dependències. És l'error de rendiment més car i més freqüent. Dependències a dalt, codi a baix.
  • Confiar que la memòria cau "detecta" canvis remots. Un RUN apk add curl cachejat continuarà instal·lant la versió de fa tres mesos encara que avui n'hi hagi una de nova. Per a builds de release, --pull i, si escau, --no-cache.
  • Reconstruir amb --no-cache "per si de cas" en desenvolupament. Llences a les escombraries l'avantatge de l'apartat 7. Fes-ho servir quan tinguis un motiu concret.
  • Pensar que docker build puja la imatge. No puja res. La imatge queda a la teva màquina fins que facis docker push (lliçó 02-06).
  • Consell: vigila la línia transferring context. Si creix amb el temps, és que alguna cosa nova s'està colant al context. És el xivato més barat que tens.
  • Consell: etiqueta des del primer moment. Una build sense -t produeix una imatge <none>:<none> que només pots referenciar per ID i que es converteix en brossa acumulada (imatges dangling, lliçó 02-05).

Exercicis

Exercici 1: mesura l'efecte del .dockerignore

Partint de ~/aurora-libros/api amb node_modules instal·lat:

  1. Reanomena temporalment el .dockerignore i construeix amb --no-cache --progress=plain, anotant la línia transferring context.
  2. Restaura el .dockerignore i repeteix el mesurament.
  3. Calcula el percentatge de reducció.
  4. Afegeix al projecte un fitxer .env amb una contrasenya fictícia i una carpeta captures/ amb 5 MB de dades falses. Sense tocar el .dockerignore, comprova si entren al context i raona què hauria passat amb un COPY . . si el .dockerignore no existís.

Pista per generar dades falses: dd if=/dev/urandom of=captures/dump.bin bs=1M count=5.

Exercici 2: demostra la invalidació en cascada

Amb el Dockerfile de l'apartat 10 ja construït:

  1. Reconstrueix sense canviar res i comprova que tots els passos surten CACHED.
  2. Modifica una línia de server.js i reconstrueix. Quins passos continuen a la memòria cau i quin és el primer que es reexecuta? Quant triga?
  3. Afegeix una dependència al package.json (per exemple "dotenv": "^16.4.7"), regenera el lock amb npm install --package-lock-only i reconstrueix. Quants passos es reexecuten ara? Quant triga?
  4. Canvia un comentari dins del Dockerfile, a la línia immediatament anterior al RUN npm ci. Reconstrueix. Es manté la memòria cau del RUN? Explica el resultat.

Exercici 3: arregla un Dockerfile trencat

Aquest Dockerfile té quatre problemes relacionats amb el que s'ha vist a la lliçó. Localitza'ls, explica el símptoma de cadascun i escriu la versió corregida.

FROM node:22-alpine
COPY . .
COPY ../db/init.sql /app/init.sql
RUN npm install
WORKDIR /app
CMD ["node", "server.js"]

Solucions

Solució a l'exercici 1

cd ~/aurora-libros/api

# 1. Sense .dockerignore
mv .dockerignore .dockerignore.off
docker build --no-cache --progress=plain -t mesura . 2>&1 | grep "transferring context"
#2 transferring context: 23.41MB 1.9s done
# 2. Amb .dockerignore
mv .dockerignore.off .dockerignore
docker build --no-cache --progress=plain -t mesura . 2>&1 | grep "transferring context"
#2 transferring context: 47.83kB 0.0s done

3. Reducció: (23.410 − 48) / 23.410 ≈ 99,8 %. El temps de transferència passa de ~1,9 s a un valor no mesurable. En una jornada amb cinquanta builds, són gairebé dos minuts d'espera pura recuperats, i en un pipeline de CI l'estalvi es multiplica per cada execució.

4. Amb els fitxers nous:

echo "DB_PASSWORD=superSecreta2026" > .env
mkdir -p captures && dd if=/dev/urandom of=captures/dump.bin bs=1M count=5 2>/dev/null
docker build --no-cache --progress=plain -t mesura . 2>&1 | grep "transferring context"
#2 transferring context: 5.29MB 0.2s done

El context creix 5 MB: captures/ hi entra perquè no és al .dockerignore. En canvi .env no hi entra, perquè sí que està exclòs. Comprova-ho mirant dins de la imatge:

docker run --rm mesura ls -la /app
total 60
drwxr-xr-x    1 root     root          4096 Aug  4 10:22 .
drwxr-xr-x    5 root     root          4096 Aug  4 10:22 captures
-rw-r--r--    1 root     root         44231 Aug  4 10:22 package-lock.json
-rw-r--r--    1 root     root           412 Aug  4 10:22 package.json
-rw-r--r--    1 root     root          6104 Aug  4 10:22 server.js

No hi ha .env i sí que hi ha captures/. Les dues lliçons:

  • Sense .dockerignore, aquell .env amb superSecreta2026 hauria acabat dins de la imatge i, en publicar-la al repositori públic de l'apartat 10 de la lliçó anterior, a Internet. I no n'hi hauria prou d'esborrar-lo en una capa posterior: com vas aprendre a 01-05, la capa inferior conserva el fitxer i docker image history delata l'operació.
  • El .dockerignore s'ha de mantenir. Ha protegit el que se li va demanar que protegís, però captures/ és nou i ningú no el va afegir. Afegeix captures/ al fitxer i torna a mesurar.

Solució a l'exercici 2

1. Sense canvis: [+] Building 0.4s, tots els passos CACHED. La build és pràcticament instantània perquè BuildKit només verifica claus de memòria cau.

2. Després de tocar server.js:

 => CACHED [2/5] WORKDIR /app                                     0.0s
 => CACHED [3/5] COPY package.json package-lock.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.4s
[+] Building 1.1s (11/11) FINISHED

Només es reexecuta el COPY . ., perquè el checksum del context ha canviat. El RUN npm ci continua a la memòria cau, que és justament l'objectiu del disseny. Poc més d'un segon.

3. Després de canviar package.json i el lock:

 => CACHED [2/5] WORKDIR /app                                     0.0s
 => [3/5] COPY package.json package-lock.json ./                  0.0s
 => [4/5] RUN npm ci --omit=dev && npm cache clean --force        9.2s
 => [5/5] COPY . .                                                0.1s
[+] Building 10.4s (11/11) FINISHED

S'invalida des del pas 3, i en cascada el npm ci i el COPY . .. Deu segons. I és el correcte: has canviat les dependències, així que cal reinstal·lar-les. La memòria cau no falla, funciona exactament com ha de funcionar.

4. Canviar un comentari immediatament anterior al RUN:

 => CACHED [4/5] RUN npm ci --omit=dev && npm cache clean --force 0.0s

La memòria cau es manté. La clau de memòria cau es calcula sobre el text de la instrucció, i els comentaris no són instruccions: l'intèrpret els descarta abans de calcular res. En canvi, si modifiques el text del RUN mateix —encara que només sigui afegir un espai o reordenar dues opcions equivalents—, la memòria cau sí que s'invalida, perquè la comparació és textual, no semàntica. És la raó per la qual reformatar un RUN llarg "perquè quedi més bonic" dispara una build completa.

Solució a l'exercici 3

Els quatre problemes:

  1. COPY . . abans de WORKDIR. Sense WORKDIR definit, el directori de treball és /, així que els fitxers aterren a l'arrel del sistema de fitxers, barrejats amb /bin, /etc i /usr. Després el WORKDIR /app del final crea un /app buit, i el CMD falla amb Cannot find module '/app/server.js'.
  2. COPY ../db/init.sql apunta fora del context: error de build immediat ("/db/init.sql": not found). A més, aquell fitxer no pertany a la imatge de l'API: és la inicialització de PostgreSQL i es resoldrà al mòdul 3.
  3. COPY . . abans d'instal·lar dependències. Invalidació en cascada: cada canvi a server.js força la reinstal·lació completa de dependències.
  4. npm install en lloc de npm ci --omit=dev. npm install pot resoldre versions diferents de les fixades a package-lock.json, amb la qual cosa dues builds del mateix codi poden produir imatges diferents, i instal·la també les dependències de desenvolupament, que engreixen la imatge sense aportar res en execució.

Versió corregida:

# syntax=docker/dockerfile:1
FROM node:22-alpine

# 1. WORKDIR ABANS de qualsevol COPY: fixa on aterren els fitxers
WORKDIR /app

# 2. init.sql eliminat: no pertany a aquesta imatge
# 3. Dependències primer, per preservar la memòria cau
COPY package.json package-lock.json ./

# 4. npm ci: reproduïble i sense dependències de desenvolupament
RUN npm ci --omit=dev && npm cache clean --force

# El codi, en darrer lloc per ser el més volàtil
COPY . .

ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]

Verifica que ara els fitxers són on han de ser:

docker build -t aurora-api:corregit .
docker run --rm aurora-api:corregit ls /app
node_modules
package-lock.json
package.json
server.js

Conclusió

Ja saps fabricar imatges. docker build no executa res a la teva carpeta: empaqueta el context de construcció i l'envia a BuildKit, que interpreta el Dockerfile com un graf de passos i produeix capes. Aquell punt final de la comanda és el context, no el Dockerfile, i d'aquí es deriven tant l'error de COPY fora del context com la necessitat del .dockerignore, que a Aurora Libros ha retallat l'enviament de 23,41 MB a 47,83 kB i, de passada, ha impedit que un .env amb credencials acabi dins de la imatge. L'opció -f desacobla la ubicació del Dockerfile del context, però les rutes de COPY continuen resolent-se sempre contra aquest últim.

Has vist també la peça que més temps t'estalviarà en la teva vida diària: la memòria cau de capes. BuildKit calcula una clau per instrucció a partir de la capa anterior, del text de la instrucció i del contingut dels fitxers copiats; si aquella clau existeix, el pas surt CACHED en zero segons, i si no, es reexecuta aquell pas i tots els següents. D'aquesta invalidació en cascada neix la regla d'or d'ordenar de menys a més volàtil, que has mesurat: 46,7 s davant d'1,4 s davant d'un simple canvi de codi, trenta-tres vegades més ràpid només per partir un COPY en dos. I saps quan desconfiar de la memòria cau amb --no-cache i --pull, i com llegir la sortida de BuildKit per localitzar el punt exacte en què va deixar d'haver-hi CACHED.

El més important: auroralibros/aurora-api:0.1.0 existeix. Són 167 MB que arrenquen Express en un contenidor i responen a curl http://localhost:3000/salut en una màquina on no hi ha Node instal·lat. Quatre dels quinze passos d'onboarding han caigut de cop. Que /llibres retorni ECONNREFUSED no és una fallada: és el problema següent del temari, i es resoldrà quan connectis contenidors en xarxa al mòdul 3.

Ara bé, aquest Dockerfile l'has escrit gairebé a cegues: saps què fa cada línia a grans trets, però no per què està escrita així. Per què npm ci i no npm install, per què el CMD porta claudàtors i cometes, per què EXPOSE no exposa res, quan fer servir ADD en lloc de COPY i per què encadenar comandes amb && produeix imatges més petites. Tot això és la lliçó següent, Conceptes Bàsics de Dockerfile, on recorreràs el llenguatge instrucció per instrucció i acabaràs amb el Dockerfile definitiu d'aurora-api, comentat línia a línia i justificat decisió a decisió.

Docker: De Principiant a Avançat

Mòdul 1: Introducció a Docker

Mòdul 2: Treballant amb Imatges Docker

Mòdul 3: Contenidors Docker

Mòdul 4: Docker Compose

Mòdul 5: Conceptes Avançats de Docker

Mòdul 6: Docker en Producció

Mòdul 7: Ecosistema i Eines de Docker

© Copyright 2026. Tots els drets reservats