La imagen auroralibros/aurora-api:1.2.0 pesa 142 MB. No es un desastre, pero tampoco está optimizada: dentro viaja un compilador de C, las dependencias de desarrollo y ficheros que nadie ejecuta. En esta lección la pones a dieta midiendo en cada paso, hasta una cifra concreta, sin perder nada del endurecimiento de la lección 05-03.

Contenido

  1. Por qué importa el tamaño y qué no hay que sacrificar
  2. Medir primero: image ls e history
  3. dive: explorar capa a capa
  4. Elección de la base, con cifras
  5. Construcciones multietapa: el concepto
  6. Multietapa aplicada a aurora-api
  7. Etapas con nombre y --target
  8. Imagen base común para varias imágenes
  9. Reducir capas y contenido
  10. Borrar un fichero no reduce el tamaño (demostrado)
  11. Ordenar para la caché
  12. Verificación final y tabla resumen

  1. Por qué importa el tamaño y qué no hay que sacrificar

Motivo Impacto concreto
Tiempo de descarga Cada nodo nuevo descarga la imagen entera la primera vez
Coste de almacenamiento Registro, caché de cada nodo y cada versión conservada
Superficie de ataque Menos paquetes, menos CVE (lección 05-03)
Velocidad de despliegue Un rollback urgente es tan rápido como la descarga
Escalado automático Un pico de tráfico exige arrancar réplicas ya

En una plataforma con cinco nodos y quince despliegues al mes, bajar 40 MB por imagen son unos 3 GB menos de transferencia mensual y decenas de segundos menos en cada arranque en frío.

Y lo que no se sacrifica por bajar de peso: el HEALTHCHECK, el usuario sin privilegios, las etiquetas OCI de trazabilidad, la capacidad de diagnosticar un problema en producción y la reproducibilidad de la construcción. Una imagen de 40 MB que nadie sabe depurar a las tres de la madrugada es un mal negocio.

  1. Medir primero: image ls e history

docker image ls auroralibros/aurora-api --format "{{.Tag}}\t{{.Size}}"
docker image history auroralibros/aurora-api:1.2.0 --format "{{.Size}}\t{{.CreatedBy}}" | head -8
1.2.0   142MB
21.4MB   RUN npm ci --omit=dev
3.1MB    COPY src/ ./src/
1.8MB    RUN apk add --no-cache tini
0B       ENV NODE_ENV=production
0B       USER node
0B       HEALTHCHECK &{["CMD-SHELL" "wget -qO- ..."]}
115MB    /bin/sh -c #(nop) ADD file:... in /

El diagnóstico es inmediato y sorprende a casi todo el mundo la primera vez: de los 142 MB, 115 MB son la imagen base. Las dependencias son 21 MB y el código propio, 3 MB. La palanca principal no está en tu código: está en qué base eliges y en qué añades encima.

Para comparar con precisión usa el tamaño real (docker image inspect --format '{{.Size}}' | numfmt --to=iec) y no el que muestra el registro, que está comprimido y siempre parece menor.

  1. dive: explorar capa a capa

docker history dice cuánto pesa cada capa; dive dice qué hay dentro y cuánto se desperdicia.

docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive:latest auroralibros/aurora-api:1.2.0
│ Image Details │
Total Image size: 142 MB
Potential wasted space: 9.7 MB
Image efficiency score: 93 %

Count   Total Space  Path
    2         6.1 MB /app/node_modules/.cache
    2         2.4 MB /root/.npm/_cacache
    3         1.2 MB /app/.git

Tres hallazgos y tres decisiones: la caché de npm no debería estar en la imagen final, node_modules/.cache es basura de compilación, y .git no debería haber entrado nunca en el contexto de construcción (falta una línea en .dockerignore).

El efficiency score mide cuánto contenido se escribe en una capa y se sobrescribe o borra en otra; por debajo del 95 % hay trabajo que hacer. Para automatizarlo en CI, añade -e CI=true y --lowestEfficiency=0.95: dive termina con código distinto de cero si no se alcanza el umbral.

  1. Elección de la base, con cifras

Base Tamaño libc Herramientas Cuándo elegirla
node:22 ~1,1 GB glibc Todas: git, compiladores, curl Solo como etapa de compilación
node:22-slim ~230 MB glibc Mínimas de Debian Dependencias nativas exigentes con glibc
node:22-alpine ~135 MB musl BusyBox, apk Caso general: el equilibrio
distroless/nodejs22 ~110 MB glibc Ninguna Producción endurecida
scratch 0 MB Ninguna Solo binarios estáticos (Go, Rust)

La contrapartida de Alpine tiene nombre: musl en lugar de glibc. Consecuencias reales:

  • Los paquetes npm con binarios precompilados para glibc pueden no funcionar y tener que compilarse al instalar (más lento, y exige build-base).
  • Algunas cargas intensivas en DNS o en resolución de nombres se comportan de forma distinta con musl.
  • Ciertas bibliotecas científicas y de aprendizaje automático no soportan musl.

aurora-api usa Express, pg y redis, ninguna con dependencias nativas problemáticas: Alpine es la elección correcta. Si un día una dependencia diera problemas, node:22-slim cuesta 95 MB más y los resuelve.

  1. Construcciones multietapa: el concepto

Una construcción multietapa usa varias imágenes base en el mismo Dockerfile. Las etapas intermedias compilan y preparan; la etapa final copia solo el resultado. Todo lo demás —compiladores, cachés, dependencias de desarrollo, código fuente— se queda fuera de la imagen publicada.

flowchart LR
  subgraph E1["Etapa: dependencias (node:22-alpine)"]
    A["package*.json"] --> B["npm ci --omit=dev<br/>+ build-base para compilar"]
  end
  subgraph E2["Etapa: desarrollo"]
    C["npm ci completo<br/>devDependencies, pruebas"]
  end
  subgraph E3["Etapa final (node:22-alpine)"]
    D["COPY --from=dependencias node_modules"]
    E["COPY src/"]
  end
  B -.->|"COPY --from"| D
  E3 --> F["Imagen publicada:<br/>sin compiladores ni cachés"]
  E2 -.->|"solo con --target"| G["Imagen de desarrollo"]

Dos reglas que resumen el mecanismo:

  • Solo la última etapa (o la indicada con --target) acaba en la imagen; las demás se descartan.
  • COPY --from=<etapa> trae ficheros concretos de una etapa anterior, y nada más viaja: ni sus capas, ni su historial, ni sus secretos.

  1. Multietapa aplicada a aurora-api

Punto de partida (una sola etapa, 142 MB) y destino:

# syntax=docker/dockerfile:1
ARG NODE_VERSION=22

# ---------- Etapa 1: dependencias de producción ----------
FROM node:${NODE_VERSION}-alpine AS dependencias
WORKDIR /app
# build-base solo vive aquí: compila lo que haga falta y NO viaja a la final
RUN apk add --no-cache --virtual .build python3 make g++
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force

# ---------- Etapa 2: desarrollo y pruebas (no se publica) ----------
FROM node:${NODE_VERSION}-alpine AS desarrollo
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "--watch", "src/server.js"]

# ---------- Etapa 3: imagen final, mínima ----------
FROM node:${NODE_VERSION}-alpine AS produccion
ARG VERSION=1.3.0
ARG REVISION=desconocida
LABEL org.opencontainers.image.title="aurora-api" \
      org.opencontainers.image.version="${VERSION}" \
      org.opencontainers.image.revision="${REVISION}" \
      org.opencontainers.image.source="https://github.com/aurora-libros/aurora-api"
WORKDIR /app
ENV NODE_ENV=production PORT=3000
COPY --from=dependencias --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node package.json ./
COPY --chown=node:node src/ ./src/
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/salud || exit 1
ENTRYPOINT ["node"]
CMD ["src/server.js"]
docker build -t auroralibros/aurora-api:1.3.0 --build-arg VERSION=1.3.0 ./api
docker image ls auroralibros/aurora-api --format "{{.Tag}}\t{{.Size}}"
1.3.0   121MB
1.2.0   142MB

142 MB → 121 MB: un 15 % menos con un solo cambio estructural. ¿De dónde salen los 21 MB? De build-base (python3, make, g++), que se instalaba en la imagen final y ahora vive y muere en la etapa dependencias, y de la caché de npm.

Fíjate en que nada de lo endurecido se ha perdido: USER node, el HEALTHCHECK, las etiquetas OCI y el ENTRYPOINT/CMD en forma exec siguen ahí. Optimizar no es recortar seguridad.

  1. Etapas con nombre y --target

Las etapas con nombre no son solo organización: son artefactos construibles por separado.

docker build --target desarrollo -t aurora-api:dev ./api
docker build --target dependencias -t aurora-api:deps ./api
docker build -t auroralibros/aurora-api:1.3.0 ./api      # última etapa
docker image ls aurora-api --format "{{.Tag}}\t{{.Size}}"
dev     318MB
deps    198MB

La imagen de desarrollo pesa 318 MB y está bien que así sea: lleva las devDependencies, el marco de pruebas y las herramientas de depuración. Es exactamente el target: desarrollo que compose.override.yaml usa desde la lección 04-07, y ahora ves de dónde sale.

Un patrón muy útil es una etapa de pruebas que falla la construcción si las pruebas fallan:

FROM desarrollo AS pruebas
RUN npm test
docker build --target pruebas ./api || echo "BUILD ROTA: pruebas en rojo"

BuildKit no ejecuta esa etapa al construir la de producción, porque no forma parte de su grafo de dependencias (lección 05-05). Solo se ejecuta si la pides con --target.

  1. Imagen base común para varias imágenes

Cuando publicas varios servicios en Node, conviene una base propia con lo compartido:

# aurora-base/Dockerfile -> auroralibros/aurora-base-node:22
FROM node:22-alpine
RUN apk add --no-cache tini tzdata && \
    addgroup -g 1001 aurora && adduser -u 1001 -G aurora -s /bin/sh -D aurora
ENV TZ=Europe/Madrid NODE_ENV=production
ENTRYPOINT ["/sbin/tini", "--"]
FROM auroralibros/aurora-base-node:22 AS produccion

Ventajas: una sola actualización de seguridad se propaga a todos los servicios, la capa base se comparte en disco y en la descarga entre ellos, y las decisiones comunes están en un solo sitio. Inconveniente: creas una dependencia interna que hay que mantener y versionar. Compensa a partir de tres o cuatro servicios.

  1. Reducir capas y contenido

# ❌ Tres capas, y la caché de apk queda dentro para siempre
RUN apk update
RUN apk add curl
RUN rm -rf /var/cache/apk/*

# ✅ Una capa, sin caché: lo que no se escribe no ocupa
RUN apk add --no-cache curl
Técnica Qué hace Ahorro típico
apk add --no-cache Evita escribir el índice de paquetes 5-10 MB
apt-get ... && rm -rf /var/lib/apt/lists/* en el mismo RUN Igual en Debian 30-50 MB
--no-install-recommends No instala paquetes "sugeridos" 20-100 MB
npm ci --omit=dev Sin dependencias de desarrollo 40-70 % de node_modules
npm cache clean --force Elimina ~/.npm/_cacache 10-30 MB
--virtual .build + apk del .build Compiladores temporales en la misma capa 100-200 MB
.dockerignore afinado Impide que entre lo que no toca Variable, a veces enorme

El .dockerignore de aurora-api tras lo que reveló dive:

.git
.gitignore
node_modules
npm-debug.log
Dockerfile*
compose*.yaml
.env
.env.*
!.env.example
coverage/
.nyc_output/
*.md
!README.md
.vscode/
.idea/
pruebas/
**/*.test.js
du -sh api/ && docker build -t aurora-api:ctx ./api 2>&1 | grep "transferring context"
94M     api/
=> transferring context: 812.43kB

De 94 MB de directorio a 812 kB de contexto. Eso no solo hace la construcción más rápida: garantiza que .git y .env no puedan colarse en ninguna capa, que es un asunto de seguridad además de tamaño.

  1. Borrar un fichero no reduce el tamaño (demostrado)

FROM alpine:3
RUN dd if=/dev/zero of=/temporal.bin bs=1M count=100    # capa A: +100 MB
RUN rm /temporal.bin                                     # capa B: whiteout
docker build -q -t borrado:mal /tmp/borrado
docker image ls borrado:mal --format "{{.Size}}"
docker run --rm borrado:mal ls -la /temporal.bin 2>&1
docker image history borrado:mal --format "{{.Size}}\t{{.CreatedBy}}" | head -3
113MB
ls: /temporal.bin: No such file or directory
0B       RUN rm /temporal.bin
105MB    RUN dd if=/dev/zero of=/temporal.bin bs=1M count=100
8MB      /bin/sh -c #(nop) ADD file:... in /

El fichero no existe y la imagen sigue pesando 113 MB. La explicación es la de la lección 05-02: las capas son inmutables y rm en una capa posterior solo crea un whiteout que oculta el fichero; los 105 MB siguen ahí abajo, se descargan en cada pull y se almacenan en cada nodo.

La corrección es unir creación y borrado en la misma instrucción:

FROM alpine:3
RUN dd if=/dev/zero of=/temporal.bin bs=1M count=100 && \
    echo "usar el fichero..." && \
    rm /temporal.bin
docker build -q -t borrado:bien /tmp/borrado2 && docker image ls borrado:bien --format "{{.Size}}"
8.15MB

De 113 MB a 8,15 MB. La misma lógica, el mismo resultado funcional, un && de diferencia. Y es exactamente el motivo de --virtual .build en el Dockerfile del apartado 6: instalar, compilar y desinstalar dentro de un solo RUN.

  1. Ordenar para la caché

Recordatorio de la lección 02-02, ahora combinado con multietapa: las instrucciones se ordenan de menos a más cambiante.

COPY package.json package-lock.json ./   # cambia poco
RUN npm ci --omit=dev                    # capa cara, reutilizable
COPY src/ ./src/                         # cambia en cada commit

Con multietapa el efecto se multiplica: al tocar src/server.js, BuildKit reutiliza toda la etapa dependencias —incluida la instalación de npm— y solo rehace las dos últimas capas de la etapa final.

touch api/src/server.js
time docker build -t auroralibros/aurora-api:1.3.0 ./api
 => CACHED [dependencias 4/4] RUN npm ci --omit=dev
 => [produccion 5/5] COPY --chown=node:node src/ ./src/     0.2s
real    0m3.184s

Tres segundos frente a los cuarenta y tantos de una construcción limpia. Optimizar el tamaño y optimizar la velocidad de construcción son, casi siempre, el mismo trabajo.

  1. Verificación final y tabla resumen

Una imagen optimizada que no funciona no vale nada. La comprobación es obligatoria:

docker compose up -d --wait
docker compose exec aurora-api node --version
curl -s http://localhost:8080/api/salud | jq -c
curl -s http://localhost:8080/api/libros | jq -c '{origen, total}'
curl -s http://localhost:8080/api/libros/3 | jq -c '{titulo, autor}'
docker inspect aurora-libros-aurora-api-1 --format 'user={{.Config.User}} salud={{.State.Health.Status}}'
v22.11.0
{"estado":"ok","bd":"conectada","cache":"conectada","version":"1.3.0"}
{"origen":"db","total":9}
{"titulo":"Cien años de soledad","autor":"Gabriel García Márquez"}
user=node salud=healthy

Y el último escalón, si tu contexto lo permite: cambiar la base final a distroless.

FROM gcr.io/distroless/nodejs22-debian12 AS produccion
COPY --from=dependencias --chown=1000:1000 /app/node_modules /app/node_modules
COPY --chown=1000:1000 src/ /app/src/
WORKDIR /app
USER 1000
CMD ["src/server.js"]        # distroless ya tiene node como ENTRYPOINT
docker build -t auroralibros/aurora-api:1.3.0-distroless ./api
docker image ls auroralibros/aurora-api --format "{{.Tag}}\t{{.Size}}"
1.3.0-distroless   102MB
1.3.0              121MB
1.2.0              142MB

142 MB → 102 MB: un 28 % menos. El precio, ya conocido de la lección 05-03: sin sh no hay docker exec ... sh y el HEALTHCHECK con wget deja de funcionar, así que la sonda pasa a hacerse desde Compose o desde el orquestador.

Técnica Ahorro típico Coste
Base -alpine en vez de completa 800-900 MB musl en vez de glibc
Multietapa (fuera compiladores y devDependencies) 20-40 % Dockerfile algo más largo
--virtual + apk del en el mismo RUN 100-200 MB Ninguno
npm ci --omit=dev + cache clean 40-70 % de node_modules Ninguno
.dockerignore afinado Variable (aquí, 93 MB de contexto) Hay que mantenerlo
Agrupar RUN con && Lo que ocupe lo temporal Menos granularidad de caché
Base distroless 15-25 MB más Sin shell: depuración difícil
Fusionar todas las capas Poco Rompe la caché: casi nunca compensa

Errores Comunes y Consejos

Optimizar sin medir. Sin docker history y dive, se recorta donde no duele y se deja intacta la capa de 100 MB.

Borrar ficheros en un RUN posterior. El fichero desaparece y el espacio no. Creación y borrado, en la misma instrucción.

Copiar todo el proyecto con COPY . . sin .dockerignore. Entran .git, node_modules y, con suerte, algún .env.

Instalar herramientas de compilación en la imagen final. Compiladores en producción son peso y superficie de ataque. Van en la etapa de dependencias.

Sacrificar el HEALTHCHECK o el USER por ahorrar unos megas. El intercambio es pésimo: la seguridad y la operabilidad valen más que 3 MB.

Perseguir el mínimo absoluto. Un binario en scratch que no puedes depurar cuesta más caro en la primera incidencia de lo que ahorró en un año de descargas.

Consejo: integra la medición en el flujo de trabajo. Un paso de CI que compare el tamaño de la imagen con el de la versión anterior y avise si crece más de un 10 % detecta la dependencia inflada el día que entra, no seis meses después.

Ejercicios

Ejercicio 1. Analiza auroralibros/aurora-api:1.2.0 con history y con dive: identifica la capa más grande, calcula qué porcentaje del total representa la base, y encuentra al menos un desperdicio real que puedas eliminar con .dockerignore.

Ejercicio 2. Convierte el Dockerfile de aurora-api a multietapa con tres etapas (dependencias, desarrollo, produccion), mide el antes y el después, y demuestra que la imagen final no contiene el compilador que sí está en la etapa de dependencias.

Ejercicio 3. Demuestra que borrar un fichero en una capa posterior no reduce el tamaño y arréglalo. Calcula el ahorro exacto y explica qué habría pasado si ese fichero hubiera sido un fichero de credenciales.

Soluciones

Solución 1.

docker image history auroralibros/aurora-api:1.2.0 --format "{{.Size}}\t{{.CreatedBy}}" \
  | sort -h -r | head -3
total=$(docker image inspect auroralibros/aurora-api:1.2.0 --format '{{.Size}}')
base=$(docker image inspect node:22-alpine --format '{{.Size}}')
echo "base: $((base * 100 / total)) % del total"
115MB    /bin/sh -c #(nop) ADD file:... in /
21.4MB   RUN npm ci --omit=dev
3.1MB    COPY src/ ./src/
base: 81 % del total

El 81 % de la imagen es la base. Es el dato que reordena las prioridades: pelear por los 3 MB del código propio es irrelevante mientras no se decida conscientemente qué base se usa. Con dive aparece el desperdicio concreto:

Potential wasted space: 9.7 MB
    2         6.1 MB /app/node_modules/.cache
    2         2.4 MB /root/.npm/_cacache
    3         1.2 MB /app/.git

.git dentro de una imagen de producción no es solo 1,2 MB de peso: es el historial completo del repositorio viajando en el registro, con cualquier secreto que alguien commiteara y luego borrara. Se corrige con una línea en .dockerignore, y la comprobación es directa:

docker run --rm auroralibros/aurora-api:1.3.0 ls -a /app | grep -c '^\.git$'   # -> 0

Solución 2.

docker build -t aurora-api:antes -f api/Dockerfile.mono ./api
docker build -t aurora-api:despues ./api
docker image ls aurora-api --format "{{.Tag}}\t{{.Size}}"
antes     142MB
despues   121MB
# ¿Está el compilador en cada etapa?
docker build -q --target dependencias -t aurora-api:deps ./api >/dev/null
docker run --rm aurora-api:deps sh -c 'which g++ make python3 | wc -l'
docker run --rm aurora-api:despues sh -c 'which g++ make python3 2>/dev/null | wc -l'
docker run --rm aurora-api:despues sh -c 'ls node_modules | wc -l'
docker run --rm aurora-api:deps sh -c 'ls node_modules | wc -l'
3
0
64
64

La etapa dependencias tiene los tres binarios de compilación; la final, ninguno. Y sin embargo ambas tienen los mismos 64 paquetes en node_modules: COPY --from=dependencias ha traído exactamente el resultado del trabajo y nada del utillaje que lo produjo.

Esa es la idea central de multietapa expresada de la forma más clara posible: lo que necesitas para construir no es lo que necesitas para ejecutar. Y el beneficio no es solo de tamaño; también es de seguridad, porque un atacante con ejecución de comandos en la imagen final no encuentra un compilador con el que preparar la siguiente fase de su ataque.

Solución 3.

mkdir -p /tmp/b1 /tmp/b2
printf 'FROM alpine:3\nRUN dd if=/dev/zero of=/t.bin bs=1M count=100\nRUN rm /t.bin\n' > /tmp/b1/Dockerfile
printf 'FROM alpine:3\nRUN dd if=/dev/zero of=/t.bin bs=1M count=100 && rm /t.bin\n' > /tmp/b2/Dockerfile
docker build -q -t mal /tmp/b1 >/dev/null && docker build -q -t bien /tmp/b2 >/dev/null
docker image ls mal bien --format "{{.Repository}}\t{{.Size}}"
docker run --rm mal ls /t.bin 2>&1
mal     113MB
bien    8.15MB
ls: /t.bin: No such file or directory

El ahorro exacto son 104,85 MB, un 92,8 % del total, y el resultado funcional es idéntico: en ambas imágenes el fichero no existe. La diferencia está en si los 100 MB llegaron a formar parte de una capa consolidada. En mal, la capa A los escribió y quedaron inmutables; la capa B solo añadió un whiteout que los oculta. En bien, el fichero nació y murió dentro de la misma instrucción, así que cuando BuildKit consolidó la capa ya no estaba.

La pregunta sobre las credenciales lleva la demostración a su conclusión más incómoda:

printf 'FROM alpine:3\nRUN echo "aurora:S3cr3t0_2026" > /cred.txt && cat /cred.txt >/dev/null\nRUN rm /cred.txt\n' > /tmp/b1/Dockerfile
docker build -q -t cred /tmp/b1 >/dev/null
docker run --rm cred ls /cred.txt 2>&1
docker save cred | tar -xO --wildcards '*/layer.tar' 2>/dev/null | strings | grep -o 'S3cr3t0_[0-9]*'
ls: /cred.txt: No such file or directory
S3cr3t0_2026

El fichero no existe y la contraseña se recupera de la capa exportada en un solo comando. Si esa imagen se hubiera publicado en Docker Hub, cualquiera podría extraerla con docker pull y docker save, sin necesidad de ejecutar el contenedor. Por eso el mismo && que ahorra 105 MB es también un control de seguridad, y por eso la solución correcta para los secretos de construcción no es un && sino RUN --mount=type=secret, que verás en la lección siguiente.

Conclusión

aurora-api ha pasado de 142 MB a 102 MB, un 28 % menos, sin perder el usuario sin privilegios, el HEALTHCHECK, las etiquetas OCI ni un solo endpoint: /salud, /libros y /libros/:id responden igual y el contenedor sigue llegando a healthy. Y, más importante que la cifra, tienes el método: medir primero. docker image ls para el total, docker image history para encontrar la capa gorda —que resultó ser la base, con el 81 % del peso— y dive para ver el desperdicio real, que destapó la caché de npm y un .git que nunca debió entrar en el contexto.

Sabes elegir la base con criterio y no por costumbre, conociendo la contrapartida de musl frente a glibc y qué cuesta cada escalón entre node:22, -slim, -alpine, distroless y scratch. Dominas las construcciones multietapa: una etapa que instala y compila con build-base, una de desarrollo con las devDependencies que alimenta el target: desarrollo de tu compose.override.yaml, una etapa de pruebas que rompe la construcción si algo falla, y una final que solo recibe node_modules y src/ mediante COPY --from —demostrado: tres compiladores en la etapa intermedia, cero en la publicada—. Y sabes construir cada una por separado con --target, además de compartir una imagen base propia entre varios servicios.

Del lado del contenido: --no-cache, --no-install-recommends, --virtual con su apk del, npm ci --omit=dev, npm cache clean y un .dockerignore que redujo el contexto de 94 MB a 812 kB. Y la demostración que más vale la pena recordar: borrar un fichero en una capa posterior no reduce el tamaño —113 MB frente a 8,15 MB por un &&—, y ese mismo mecanismo hace que una credencial escrita y borrada siga siendo extraíble de la imagen publicada.

En la lección 05-05 entra en escena el constructor que ha estado haciendo todo esto por debajo: BuildKit, con su grafo de dependencias y su paralelismo entre etapas, y Buildx con sus builders. Verás los montajes en RUNtype=cache para no reinstalar npm en cada build, type=bind, type=tmpfs y sobre todo type=secret, la respuesta correcta al problema de credenciales que acabas de demostrar—, la caché remota compartida con --cache-from y --cache-to, la compilación multiarquitectura para amd64 y arm64 con sus manifest lists, y docker buildx bake para construir todas las imágenes de Aurora Libros de una sola vez.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados