En la lección anterior aprendiste de dónde vienen las imágenes: un registro, un repositorio, una etiqueta que apunta a un digest. Y reservaste la dirección donde vivirá la tuya, auroralibros/aurora-api. Ahora toca fabricarla. Esta lección trata del proceso de construcción en sí: qué hace docker build desde que pulsas Enter, qué es ese punto final que todo el mundo copia sin saber qué significa, por qué una carpeta con node_modules dentro puede convertir una build de dos segundos en una de dos minutos, y cómo la caché de capas de BuildKit premia o castiga el orden en que escribes las instrucciones. No vas a estudiar todavía el Dockerfile instrucción por instrucción —eso es la lección siguiente—, sino la máquina que lo interpreta. Al terminar tendrás auroralibros/aurora-api:0.1.0 construida, corriendo en un contenedor y respondiendo a un curl.
Contenido
- Qué hace
docker buildrealmente - Anatomía del comando y el punto final
- El contexto de construcción
- El fichero
.dockerignore - Dockerfiles con otro nombre o ruta: la opción
-f - La caché de capas de BuildKit
- Dependencias antes que código: ineficiente frente a eficiente
--no-cachey--pull: cuándo desconfiar de la caché- Leer la salida de BuildKit
- De cero a imagen:
auroralibros/aurora-api:0.1.0
- Qué hace
docker build realmente
docker build realmenteRecuerda la arquitectura cliente-servidor de la lección 01-03: el cliente docker no hace el trabajo, se lo pide al demonio. La construcción de imágenes sigue ese mismo esquema, con un actor más. Desde Docker Engine 23, el constructor por defecto es BuildKit, un motor de construcción independiente y mucho más capaz que el clásico.
sequenceDiagram
participant U as Tú (terminal)
participant C as Cliente docker
participant D as dockerd
participant B as BuildKit
participant R as Registro
U->>C: docker build -t aurora-api:0.1.0 .
C->>C: Lee el Dockerfile y el .dockerignore
C->>D: Envía el contexto (ficheros no ignorados)
D->>B: Solicita la construcción
B->>B: Analiza el Dockerfile y calcula el grafo de pasos
B->>R: ¿Tengo node:22-alpine? Si no, la descarga
R-->>B: Capas de la imagen base
loop Por cada instrucción
B->>B: ¿Hay caché válida para este paso?
alt Hay caché
B-->>B: CACHED (reutiliza la capa)
else No hay caché
B->>B: Ejecuta el paso y crea una capa nueva
end
end
B->>D: Exporta las capas y el manifiesto
D->>D: Guarda la imagen y le pone la etiqueta
D-->>C: Build completada
C-->>U: naming to docker.io/auroralibros/aurora-api:0.1.0
Tres consecuencias prácticas de este diagrama que conviene interiorizar antes de seguir:
- La build no ocurre en tu directorio de trabajo. Ocurre en el constructor, que puede estar en otra máquina. Por eso hay que enviarle los ficheros: es el contexto del apartado 3.
- BuildKit construye un grafo, no una lista. No ejecuta las instrucciones ciegamente de arriba abajo: calcula qué pasos dependen de cuáles y salta los que ya tiene resueltos. De ahí la potencia de la caché.
- La imagen resultante se queda en tu almacén local.
docker buildno publica nada en ningún registro; eso esdocker push, y llega en la lección 02-06.
docker build y docker buildx build
Te encontrarás las dos formas y conviene aclararlas ya:
| Comando | Qué es |
|---|---|
docker build |
El comando clásico. Desde Engine 23 delega en BuildKit de forma transparente |
docker buildx build |
La interfaz completa de BuildKit, con opciones adicionales (multiplataforma, exportadores, constructores remotos) |
Para todo lo de este módulo son equivalentes: docker build es más corto y es lo que usarás. Las capacidades exclusivas de buildx —construir para varias arquitecturas a la vez, montajes de caché, secretos de build— se ven en la lección 05-05. Puedes comprobar qué constructor estás usando:
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default* docker
\_ default \_ default running v0.19.0 linux/amd64, linux/386El asterisco marca el constructor activo. BUILDKIT v0.19.0 confirma que BuildKit está en marcha; si esa columna estuviera vacía, estarías con el constructor antiguo y no verías nada de lo que se describe en esta lección.
- Anatomía del comando y el punto final
La forma canónica es:
Desmenuzada:
| Parte | Qué es | Detalle |
|---|---|---|
docker build |
El comando | Construye una imagen |
-t auroralibros/aurora-api:0.1.0 |
--tag: nombre y etiqueta de la imagen resultante |
Se puede repetir para dar varios nombres a la misma build |
. |
El contexto de construcción | El directorio cuyo contenido se envía al constructor |
El punto final es la parte que más confusión genera. No significa "construye aquí" ni "usa el Dockerfile de este directorio", aunque por defecto produzca ambos efectos. Significa: el contexto de construcción es el directorio actual. Es decir, "envía al constructor todo lo que hay en . (menos lo ignorado), porque las instrucciones COPY van a buscar sus ficheros ahí dentro".
Se ve mejor con variantes:
# Contexto = directorio actual; Dockerfile = ./Dockerfile
docker build -t aurora-api:0.1.0 .
# Contexto = ./api ; Dockerfile = ./api/Dockerfile
docker build -t aurora-api:0.1.0 ./api
# Contexto = directorio padre; Dockerfile = ../Dockerfile
docker build -t aurora-api:0.1.0 ..
# Contexto = un repositorio Git remoto, clonado por el constructor
docker build -t aurora-api:0.1.0 https://github.com/auroralibros/aurora-libros.git#main:apiLa última forma es reveladora: el contexto ni siquiera tiene que estar en tu máquina. BuildKit puede clonar un repositorio y construir desde ahí. Eso demuestra que el contexto es un concepto abstracto ("el conjunto de ficheros disponibles para la build"), no "la carpeta donde estás".
Otras opciones de docker build que usarás en este módulo:
| Opción | Para qué |
|---|---|
-t, --tag |
Nombre y etiqueta; repetible |
-f, --file |
Ruta a un Dockerfile con otro nombre o ubicación (apartado 5) |
--no-cache |
Ignora la caché por completo (apartado 8) |
--pull |
Fuerza a descargar la versión más reciente de la imagen base (apartado 8) |
--progress=plain |
Salida completa, sin la interfaz dinámica (apartado 9) |
--build-arg |
Pasa un argumento de construcción (lección 02-04) |
--target |
Construye hasta una etapa concreta (multi-etapa, lección 05-04) |
- El contexto de construcción
Antes de ejecutar ninguna instrucción, el cliente empaqueta el contexto y se lo envía al constructor. Esto es importante porque todo lo que hay en ese directorio viaja, lo uses o no en algún COPY.
Comprueba el tamaño de tu contexto antes de construir. Es un hábito que vale su peso en oro:
Veintitrés megas para un server.js de 100 líneas y un package.json. ¿De dónde salen?
Ahí está: node_modules, generado cuando hiciste npm install en la lección 01-07. Y esos 23 MB no aportan nada a la imagen, porque las dependencias se instalarán dentro de ella con npm ci.
Consecuencias de un contexto grande:
- La build es más lenta, y el retraso está antes de la primera instrucción: el cliente debe leer, comprimir y transmitir todos esos ficheros. En un proyecto con
.gitde 500 MB, esto son varios segundos en cada build. - Riesgo de filtración. Un
COPY . .descuidado mete en la imagen todo lo que haya en el contexto, incluidos ficheros.env, claves SSH o volcados de base de datos. Es un incidente de seguridad real, no teórico. - Rompe la caché. Como verás en el apartado 6, la caché de un
COPY . .se invalida si cualquier fichero del contexto cambia. Connode_modulesdentro, unnpm installlocal invalida la build entera.
El error clásico: COPY fuera del contexto
Este error lo vas a cometer, así que mejor entenderlo ahora. Supón que desde ~/aurora-libros/api intentas copiar el init.sql que está en ~/aurora-libros/db/:
ERROR: failed to solve: failed to compute cache key: failed to calculate checksum of
ref ...: "/db/init.sql": not foundEl mensaje despista porque habla de "not found", pero el fichero existe perfectamente en tu disco. Lo que ocurre es que no existe en el contexto: como el contexto es . (o sea, api/), el constructor solo ha recibido lo que hay dentro de api/, y ../db/ está fuera. Las rutas de origen de COPY son siempre relativas a la raíz del contexto y jamás pueden salir de él. Es una restricción de seguridad deliberada: si no fuera así, cualquier Dockerfile podría copiar /etc/shadow o ~/.ssh/id_rsa de la máquina que construye.
Las dos soluciones legítimas:
# a) Ampliar el contexto a la raíz del proyecto y apuntar al Dockerfile con -f
cd ~/aurora-libros
docker build -t prueba -f api/Dockerfile .
# b) Dejar el contexto en api/ y copiar el fichero ahí (si de verdad pertenece a la API)Para Aurora Libros elegiremos la opción de contexto reducido: la imagen de la API no necesita init.sql, que es cosa del contenedor de PostgreSQL (módulo 3). Cada imagen lleva solo lo suyo.
- El fichero
.dockerignore
.dockerignoreEn la lección 01-07 quedó anunciado: la imagen de la API tendrá un .dockerignore que excluya node_modules/, .git/ y .env. Es el momento de escribirlo.
.dockerignore es un fichero de texto en la raíz del contexto que le dice al cliente qué no debe enviar al constructor. Su sintaxis recuerda a .gitignore, pero tiene reglas propias:
| Patrón | Qué excluye |
|---|---|
node_modules |
Cualquier fichero o carpeta con ese nombre en la raíz del contexto |
**/node_modules |
Ese nombre en cualquier nivel de profundidad |
*.log |
Todos los ficheros con extensión .log en la raíz |
**/*.log |
Todos los .log en cualquier subdirectorio |
.git |
El directorio de Git completo |
temp? |
temp1, tempA… (? = un carácter cualquiera) |
!importante.log |
Excepción: vuelve a incluir ese fichero aunque un patrón anterior lo excluyera |
# comentario |
Línea ignorada |
Dos diferencias con .gitignore que causan sorpresas:
node_modulesno es recursivo por sí solo, a diferencia de Git. Si tienes subproyectos con sus propias dependencias, necesitas**/node_modules.- El orden importa cuando usas
!: la última regla que coincide es la que manda.
Crea ~/aurora-libros/api/.dockerignore:
# Dependencias: se instalan dentro de la imagen con npm ci
node_modules
**/node_modules
# Control de versiones
.git
.gitignore
# Secretos y configuración local: JAMÁS dentro de una imagen
.env
.env.*
# Registros y temporales
*.log
npm-debug.log*
.npm
.cache
tmp/
# Metadatos del sistema operativo y del editor
.DS_Store
Thumbs.db
.vscode
.idea
# El propio Dockerfile y sus auxiliares: el constructor ya los tiene
Dockerfile
Dockerfile.*
.dockerignore
# Pruebas y documentación: no se ejecutan en producción
test/
*.test.js
README.md
NOTAS-ONBOARDING.mdJustificación de las entradas menos evidentes:
node_modules: la razón principal. Además del tamaño, copiar elnode_modulesde tu máquina puede meter binarios compilados para tu sistema operativo y arquitectura que no funcionen dentro de una imagen Alpine (es exactamente el aviso del ejercicio 3 de 01-07)..env: la regla innegociable del proyecto. Las credenciales no entran en la imagen. Un.dockerignoreque lo excluya es la primera línea de defensa frente a unCOPY . .descuidado.Dockerfile: no hace falta dentro de la imagen. Excluirlo tiene un efecto secundario muy útil: editar el Dockerfile ya no invalida la caché delCOPY . ..README.md,test/: no se ejecutan en producción, y cada byte que no entra es un byte que no se descarga en cada despliegue.
Medir el antes y el después
La forma limpia de comprobar el efecto es con --progress=plain, que muestra la línea de transferencia del contexto. Primero, sin .dockerignore:
cd ~/aurora-libros/api
mv .dockerignore .dockerignore.off
docker build --no-cache --progress=plain -t medicion:sin -f Dockerfile . 2>&1 | grep -i "transferring context"Ahora con él:
mv .dockerignore.off .dockerignore
docker build --no-cache --progress=plain -t medicion:con -f Dockerfile . 2>&1 | grep -i "transferring context"De 23,41 MB a 47,83 kB: una reducción de más del 99 %, y el tiempo de transferencia baja de 1,8 segundos a prácticamente cero. En un proyecto real con .git voluminoso, artefactos de compilación y capturas de pantalla, la diferencia se mide en cientos de megas y decenas de segundos en cada build.
- Dockerfiles con otro nombre o ruta: la opción
-f
-fPor defecto, docker build busca un fichero llamado exactamente Dockerfile en la raíz del contexto. -f rompe ese acoplamiento:
# Un Dockerfile con otro nombre, en el mismo directorio
docker build -t aurora-api:dev -f Dockerfile.dev .
# Contexto en la raíz del proyecto, Dockerfile dentro de api/
cd ~/aurora-libros
docker build -t auroralibros/aurora-api:0.1.0 -f api/Dockerfile ./api
# Dockerfiles centralizados en una carpeta aparte
docker build -t aurora-web:0.1.0 -f docker/web.Dockerfile ./webPuntos clave:
-fy el contexto son independientes. Puedes tener el Dockerfile en cualquier sitio y el contexto en otro. Lo que nunca cambia es que las rutas deCOPYse resuelven contra el contexto, no contra la ubicación del Dockerfile. Es la confusión número uno con-f.- La ruta de
-fsí es relativa a tu directorio actual, no al contexto. - El caso de uso más común es tener variantes:
Dockerfilepara producción yDockerfile.devcon herramientas de desarrollo. Aurora Libros usará un único Dockerfile; las diferencias entre entornos se resolverán con Compose (lección 04-06), que es un enfoque más limpio.
- La caché de capas de BuildKit
Aquí está la diferencia entre esperar noventa segundos en cada cambio de código o esperar dos. Recuerda de la lección 01-05 que una imagen es una pila de capas. Durante la construcción, cada instrucción del Dockerfile que modifica el sistema de archivos produce una capa, y BuildKit intenta reutilizar las que ya tiene.
Cómo decide BuildKit si reutiliza una capa
Para cada instrucción, BuildKit calcula una clave de caché a partir de:
- La clave de la capa anterior (por eso el orden lo es todo).
- El texto literal de la instrucción. Cambiar un espacio o un comentario dentro de un
RUNla invalida. - Para
COPYyADD, además, el checksum del contenido de los ficheros copiados (no la fecha de modificación: si tocas un fichero sin cambiar su contenido, la caché aguanta).
Si esa clave existe en la caché local, marca el paso como CACHED y no ejecuta nada. Si no existe, ejecuta el paso y todos los posteriores, sin excepción.
La invalidación en cascada
Esta es la propiedad que gobierna el diseño de todo 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/>tu código"]
E --> F["CMD [node, server.js]"]
style C fill:#f4f0fa
style D fill:#f4f0fa
style E fill:#ffe0e0
style F fill:#ffe0e0
Si cambias una línea de server.js, se invalida el COPY . . (paso E) y, en cascada, todo lo que viene después. Pero los pasos A–D siguen cacheados, incluido el npm ci de 45 segundos. Resultado: la build tarda un par de segundos.
Ahora invierte el orden —copia el código antes de instalar dependencias— y el mismo cambio en server.js invalida el COPY . ., que ahora está antes del npm ci. Consecuencia: el npm ci se vuelve a ejecutar entero. Cuarenta y cinco segundos, en cada cambio de una coma.
De ahí la regla de oro: ordena las instrucciones de menos a más volátil. Lo que casi nunca cambia (la imagen base, la instalación de paquetes del sistema, las dependencias) va arriba; lo que cambias cincuenta veces al día (tu código) va abajo.
- Dependencias antes que código: ineficiente frente a eficiente
Vamos a medirlo, porque el número convence más que la explicación.
Versión INEFICIENTE
Parece razonable: copio el proyecto y luego instalo. Y funciona. El problema es que COPY . . incluye server.js, así que cualquier cambio en el código invalida esa capa y, con ella, el npm ci.
Versión EFICIENTE
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]La diferencia es un COPY partido en dos. Primero se copian solo los manifiestos de dependencias, se instalan, y después se copia el resto del código. Como package.json y package-lock.json cambian rara vez, el npm ci queda cacheado casi siempre.
La medición
Guarda cada versión en su fichero y cronometra el ciclo realista: primera build en frío, y segunda build tras tocar el código.
cd ~/aurora-libros/api
# Primera build de cada variante (ambas en frío)
time docker build --no-cache -q -t ineficiente -f Dockerfile.ineficiente . > /dev/null
time docker build --no-cache -q -t eficiente -f Dockerfile.eficiente . > /dev/null
# Simula un cambio de código, que es lo que pasa cincuenta veces al día
echo "// ajuste menor" >> server.js
# Reconstruye ambas, ahora aprovechando la caché
time docker build -q -t ineficiente -f Dockerfile.ineficiente . > /dev/null
time docker build -q -t eficiente -f Dockerfile.eficiente . > /dev/nullResultados típicos en una máquina de desarrollo:
| Escenario | Ineficiente | Eficiente |
|---|---|---|
| Primera build (en frío) | 48,3 s | 49,1 s |
Tras cambiar server.js |
46,7 s | 1,4 s |
Tras cambiar package.json |
47,9 s | 48,5 s |
Lee la tabla con atención, porque contiene los tres mensajes de la lección:
- En frío son iguales (la eficiente incluso un pelín más lenta, por tener un
COPYmás). La caché no acelera la primera vez. - Tras un cambio de código, la diferencia es de 33×. Multiplícalo por cincuenta builds al día y por cada persona del equipo de Aurora Libros: son horas.
- Tras cambiar las dependencias, vuelven a igualarse, y es lo correcto: si
package.jsoncambia, hay que reinstalar. La caché no está mintiendo, está haciendo su trabajo.
Un matiz sobre npm ci que justifica su uso frente a npm install, y que en la lección 02-03 se retoma: npm ci borra node_modules e instala exactamente las versiones fijadas en package-lock.json, fallando si el lock y el package.json no concuerdan. npm install puede resolver versiones distintas según cuándo se ejecute, lo que rompe la reproducibilidad. En una imagen, siempre npm ci.
--no-cache y --pull: cuándo desconfiar de la caché
--no-cache y --pull: cuándo desconfiar de la cachéLa caché es una optimización basada en una suposición: que la misma instrucción con las mismas entradas produce el mismo resultado. A veces esa suposición es falsa.
# Ignora toda la caché: ejecuta cada instrucción desde cero
docker build --no-cache -t auroralibros/aurora-api:0.1.0 .
# Comprueba si hay una versión más reciente de la imagen base y la descarga
docker build --pull -t auroralibros/aurora-api:0.1.0 .
# Ambas: la build más reproducible posible
docker build --no-cache --pull -t auroralibros/aurora-api:0.1.0 .Cuándo usar cada una:
| Situación | Opción | Por qué |
|---|---|---|
| Build de release o de CI | --pull (y a menudo --no-cache) |
Garantiza base actualizada y ausencia de estado heredado |
apt-get install / apk add que trae paquetes viejos |
--no-cache |
La instrucción es idéntica, pero los repositorios remotos han cambiado |
RUN git clone o curl de un recurso remoto |
--no-cache |
Igual: la instrucción no cambia, el contenido remoto sí |
| Sospechas de una caché corrupta o de un comportamiento inexplicable | --no-cache |
Descarta la caché como variable del problema |
| Desarrollo diario | Ninguna | Estarías tirando a la basura la ventaja del apartado 7 |
El caso de --pull merece una explicación aparte, porque es sutil. Si tu Dockerfile dice FROM node:22-alpine y ya tienes esa etiqueta descargada, BuildKit la usa sin comprobar si ha cambiado en el registro. Pero 22-alpine es una etiqueta móvil (lección 02-01): Node publica parches y esa etiqueta pasa a apuntar a otro digest. Sin --pull, puedes estar construyendo durante meses sobre una base con vulnerabilidades ya corregidas aguas arriba. Por eso --pull es prácticamente obligatorio en los pipelines de construcción (lección 06-02).
Y un aviso: --no-cache no borra la caché existente, solo la ignora en esa build. Para liberar el espacio que ocupa realmente hay un comando específico, docker builder prune, que verás en la lección 02-05.
- Leer la salida de BuildKit
BuildKit imprime una interfaz dinámica que se reescribe en el sitio. Aprender a leerla es aprender 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.0sClave de lectura, línea a línea:
| Elemento | Significado |
|---|---|
[+] Building 12.4s (11/11) FINISHED |
Tiempo total y pasos completados / totales |
[internal] … |
Pasos internos: leer el Dockerfile, el .dockerignore, resolver metadatos de la base |
transferring context: 47.83kB |
El tamaño de tu contexto. Es la línea que vigilas tras escribir el .dockerignore |
[1/5], [2/5]… |
Pasos del Dockerfile, numerados. Solo cuentan las instrucciones que generan capa |
CACHED |
Paso reutilizado de la caché. Cero segundos. Lo que quieres ver |
exporting layers |
Escritura de las capas nuevas en el almacén local |
writing image sha256:… |
El ID de la imagen resultante |
naming to … |
La etiqueta que le has puesto con -t |
El diagnóstico se hace mirando dónde deja de aparecer CACHED. Ese es el punto de invalidación, y todo lo que hay debajo se ha reejecutado. Si el primer paso no cacheado es antes de lo que esperabas, tienes un problema de orden de instrucciones o un fichero volátil colándose en un COPY.
--progress=plain
La interfaz dinámica es cómoda pero oculta la salida de los comandos: no ves lo que imprime npm ci. Cuando algo falla, necesitas verlo todo:
#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.9sEl formato es #<paso> <segundos desde el inicio del paso> <línea de salida>. Esos tiempos relativos son oro puro para saber qué parte de un RUN largo es la lenta. Tres usos habituales de --progress=plain:
- Ver por qué falla un
RUN(el mensaje de error completo del comando). - Medir el tamaño del contexto, como hiciste en el apartado 4.
- Guardar un registro completo en CI, donde la interfaz dinámica produce basura ilegible.
- De cero a imagen:
auroralibros/aurora-api:0.1.0
auroralibros/aurora-api:0.1.0Es el momento de juntarlo todo. Vas a construir la primera imagen de Aurora Libros con un Dockerfile mínimo, presentado entero. Aquí solo se explica por encima qué hace cada instrucción; el detalle completo, las alternativas y las decisiones finas son la lección 02-03.
Crea ~/aurora-libros/api/Dockerfile:
# syntax=docker/dockerfile:1
# Imagen base: Node.js 22 sobre Alpine Linux, tal como se decidió en 01-07
FROM node:22-alpine
# Directorio de trabajo dentro de la imagen; el resto de rutas son relativas a él
WORKDIR /app
# Primero SOLO los manifiestos de dependencias: así el npm ci queda cacheado
COPY package.json package-lock.json ./
# Instalación reproducible, sin dependencias de desarrollo, limpiando la caché de npm
RUN npm ci --omit=dev && npm cache clean --force
# Ahora sí, el código de la aplicación
COPY . .
# Configuración por defecto. Las credenciales NO van aquí (regla del proyecto)
ENV NODE_ENV=production
ENV PORT=3000
# Documenta que el servicio escucha en el 3000 (no publica nada por sí solo)
EXPOSE 3000
# Proceso que se ejecuta al arrancar el contenedor
CMD ["node", "server.js"]Un vistazo rápido a cada instrucción, sin entrar en profundidad:
# syntax=docker/dockerfile:1: elige el intérprete de Dockerfile más reciente de la serie 1.FROM: de qué imagen se parte.WORKDIR: fija el directorio de trabajo dentro de la imagen y lo crea si no existe.COPY: trae ficheros del contexto a la imagen. Está partido en dos por lo del apartado 7.RUN: ejecuta un comando durante la construcción y congela el resultado en una capa.ENV: define variables de entorno que persisten en ejecución.EXPOSE: documentación. No publica el puerto; eso sigue siendo cosa de-p(lección 01-06).CMD: qué proceso arranca el contenedor.
Si te preguntas por qué npm ci y no npm install, por qué esa forma de CMD con corchetes, o por qué EXPOSE no hace lo que su nombre sugiere: todo eso es exactamente el contenido de la lección 02-03.
Construir
[+] 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.0sComprueba el resultado:
167 MB, de los cuales unos 142 son la base node:22-alpine que ya conocías. Tu aplicación y sus dependencias aportan unos 25 MB.
Reconstruir y ver la caché en acción
[+] 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.0sTodo CACHED, 0,4 segundos. De 13,7 s a 0,4 s sin cambiar nada.
Ejecutar la imagen
docker run -d --name aurora-api-test -p 3000:3000 auroralibros/aurora-api:0.1.0
docker ps --filter name=aurora-api-testCONTAINER ID IMAGE STATUS PORTS NAMES
d3f8a1c9b7e2 auroralibros/aurora-api:0.1.0 Up 4 seconds 0.0.0.0:3000->3000/tcp aurora-api-testComandos ya conocidos de 01-06: -d en segundo plano, --name para poder referirte a él, -p 3000:3000 para publicar el puerto. Mira los registros:
[aurora-api] escuchando en el puerto 3000
[aurora-api] base de datos: localhost:5432/aurora_libros
[aurora-api] caché: localhost:6379La API ha arrancado dentro de un contenedor, sin que hayas instalado Node en tu máquina. Pruébala:
{"servicio":"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"}El endpoint responde (HTTP 503), que es lo que queríamos comprobar: el proceso vive, Express escucha y la ruta funciona. Pero db y cache están en ko con los mismos ECONNREFUSED de la lección 01-07, y ahora por una razón nueva y muy instructiva: localhost dentro del contenedor es el propio contenedor, no tu máquina. Ahí dentro no hay ningún PostgreSQL ni ningún Redis escuchando, y por eso la conexión se rechaza. Aunque tuvieras los servicios corriendo en tu portátil, el contenedor no los vería con esa configuración.
Y /libros, que sí necesita la base de datos:
Exactamente lo esperado. Este fallo no es un error tuyo: es el siguiente problema del curso. Conectar contenedores entre sí mediante redes propias, para que DB_HOST=aurora-db signifique algo, es el contenido de la lección 03-05; y darle a PostgreSQL un volumen donde guardar el catálogo, el de la 03-06.
Limpia antes de seguir:
La imagen se queda en tu almacén local para las lecciones siguientes. Un balance de lo conseguido: de los quince pasos manuales de la lección 01-07, los pasos 2, 3, 4 y 5 (descubrir la versión de Node, instalar nvm, instalar Node 22, ejecutar npm install) han desaparecido. Cualquiera con Docker puede ejecutar tu API sin instalar nada relacionado con Node.
Errores Comunes y Consejos
- Creer que el punto final significa "aquí está el Dockerfile". Significa "este es el contexto". Son cosas separadas, y
-flo demuestra. En cuanto lo interiorizas, la mitad de los errores deCOPYdesaparecen. COPY ../algodesde fuera del contexto. Nunca funcionará, por diseño. Amplía el contexto y usa-f, o reorganiza los ficheros. Si te ves peleando con esto, casi siempre es señal de que la imagen está intentando llevar dentro algo que no le corresponde.- Olvidar el
.dockerignore. Contexto lento, imágenes hinchadas, caché que se invalida sola y riesgo real de filtrar un.env. Escríbelo antes del primerdocker build, no después. COPY . .antes de instalar dependencias. Es el error de rendimiento más caro y más frecuente. Dependencias arriba, código abajo.- Confiar en que la caché "detecta" cambios remotos. Un
RUN apk add curlcacheado seguirá instalando la versión de hace tres meses aunque hoy haya una nueva. Para builds de release,--pully, si procede,--no-cache. - Reconstruir con
--no-cache"por si acaso" en desarrollo. Tiras a la basura la ventaja del apartado 7. Úsalo cuando tengas un motivo concreto. - Pensar que
docker buildsube la imagen. No sube nada. La imagen queda en tu máquina hasta que hagasdocker push(lección 02-06). - Consejo: vigila la línea
transferring context. Si crece con el tiempo, es que algo nuevo se está colando en el contexto. Es el chivato más barato que tienes. - Consejo: etiqueta desde el primer momento. Una build sin
-tproduce una imagen<none>:<none>que solo puedes referenciar por ID y que se convierte en basura acumulada (imágenes dangling, lección 02-05).
Ejercicios
Ejercicio 1: mide el efecto del .dockerignore
Partiendo de ~/aurora-libros/api con node_modules instalado:
- Renombra temporalmente el
.dockerignorey construye con--no-cache --progress=plain, anotando la líneatransferring context. - Restaura el
.dockerignorey repite la medición. - Calcula el porcentaje de reducción.
- Añade al proyecto un fichero
.envcon una contraseña ficticia y una carpetacapturas/con 5 MB de datos falsos. Sin tocar el.dockerignore, comprueba si entran en el contexto y razona qué habría pasado con unCOPY . .si el.dockerignoreno existiera.
Pista para generar datos falsos: dd if=/dev/urandom of=capturas/dump.bin bs=1M count=5.
Ejercicio 2: demuestra la invalidación en cascada
Con el Dockerfile del apartado 10 ya construido:
- Reconstruye sin cambiar nada y comprueba que todos los pasos salen
CACHED. - Modifica una línea de
server.jsy reconstruye. ¿Qué pasos siguen cacheados y cuál es el primero que se reejecuta? ¿Cuánto tarda? - Añade una dependencia al
package.json(por ejemplo"dotenv": "^16.4.7"), regenera el lock connpm install --package-lock-onlyy reconstruye. ¿Cuántos pasos se reejecutan ahora? ¿Cuánto tarda? - Cambia un comentario dentro del Dockerfile, en la línea inmediatamente anterior al
RUN npm ci. Reconstruye. ¿Se mantiene la caché delRUN? Explica el resultado.
Ejercicio 3: arregla un Dockerfile roto
Este Dockerfile tiene cuatro problemas relacionados con lo visto en la lección. Localízalos, explica el síntoma de cada uno y escribe la versión corregida.
FROM node:22-alpine
COPY . .
COPY ../db/init.sql /app/init.sql
RUN npm install
WORKDIR /app
CMD ["node", "server.js"]Soluciones
Solución al ejercicio 1
cd ~/aurora-libros/api
# 1. Sin .dockerignore
mv .dockerignore .dockerignore.off
docker build --no-cache --progress=plain -t medicion . 2>&1 | grep "transferring context"# 2. Con .dockerignore
mv .dockerignore.off .dockerignore
docker build --no-cache --progress=plain -t medicion . 2>&1 | grep "transferring context"3. Reducción: (23 410 − 48) / 23 410 ≈ 99,8 %. El tiempo de transferencia pasa de ~1,9 s a un valor no medible. En una jornada con cincuenta builds, son casi dos minutos de espera pura recuperados, y en un pipeline de CI el ahorro se multiplica por cada ejecución.
4. Con los ficheros nuevos:
echo "DB_PASSWORD=superSecreta2026" > .env
mkdir -p capturas && dd if=/dev/urandom of=capturas/dump.bin bs=1M count=5 2>/dev/null
docker build --no-cache --progress=plain -t medicion . 2>&1 | grep "transferring context"El contexto crece 5 MB: capturas/ entra porque no está en el .dockerignore. En cambio .env no entra, porque sí está excluido. Compruébalo mirando dentro de la imagen:
total 60
drwxr-xr-x 1 root root 4096 Aug 4 10:22 .
drwxr-xr-x 5 root root 4096 Aug 4 10:22 capturas
-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.jsNo hay .env y sí hay capturas/. Las dos lecciones:
- Sin
.dockerignore, ese.envconsuperSecreta2026habría acabado dentro de la imagen y, al publicarla en el repositorio público del apartado 10 de la lección anterior, en Internet. Y no bastaría con borrarlo en una capa posterior: como aprendiste en 01-05, la capa inferior conserva el fichero ydocker image historydelata la operación. - El
.dockerignorehay que mantenerlo. Ha protegido lo que se le pidió proteger, perocapturas/es nuevo y nadie lo añadió. Añadecapturas/al fichero y vuelve a medir.
Solución al ejercicio 2
1. Sin cambios: [+] Building 0.4s, todos los pasos CACHED. La build es prácticamente instantánea porque BuildKit solo verifica claves de caché.
2. Tras 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) FINISHEDSolo se reejecuta el COPY . ., porque el checksum del contexto ha cambiado. El RUN npm ci sigue cacheado, que es justo el objetivo del diseño. Poco más de un segundo.
3. Tras cambiar package.json y 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) FINISHEDSe invalida desde el paso 3, y en cascada el npm ci y el COPY . .. Diez segundos. Y es lo correcto: has cambiado las dependencias, así que hay que reinstalarlas. La caché no falla, funciona exactamente como debe.
4. Cambiar un comentario inmediatamente anterior al RUN:
La caché se mantiene. La clave de caché se calcula sobre el texto de la instrucción, y los comentarios no son instrucciones: el intérprete los descarta antes de calcular nada. En cambio, si modificas el texto del propio RUN —aunque solo sea añadir un espacio o reordenar dos opciones equivalentes—, la caché sí se invalida, porque la comparación es textual, no semántica. Es la razón de que reformatear un RUN largo "para que quede más bonito" dispare una build completa.
Solución al ejercicio 3
Los cuatro problemas:
COPY . .antes deWORKDIR. SinWORKDIRdefinido, el directorio de trabajo es/, así que los ficheros aterrizan en la raíz del sistema de archivos, mezclados con/bin,/etcy/usr. Luego elWORKDIR /appdel final crea un/appvacío, y elCMDfalla conCannot find module '/app/server.js'.COPY ../db/init.sqlapunta fuera del contexto: error de build inmediato ("/db/init.sql": not found). Además, ese fichero no pertenece a la imagen de la API: es la inicialización de PostgreSQL y se resolverá en el módulo 3.COPY . .antes de instalar dependencias. Invalidación en cascada: cada cambio enserver.jsfuerza la reinstalación completa de dependencias.npm installen lugar denpm ci --omit=dev.npm installpuede resolver versiones distintas de las fijadas enpackage-lock.json, con lo que dos builds del mismo código pueden producir imágenes diferentes, e instala también las dependencias de desarrollo, que engordan la imagen sin aportar nada en ejecución.
Versión corregida:
# syntax=docker/dockerfile:1
FROM node:22-alpine
# 1. WORKDIR ANTES de cualquier COPY: fija dónde aterrizan los ficheros
WORKDIR /app
# 2. init.sql eliminado: no pertenece a esta imagen
# 3. Dependencias primero, para preservar la caché
COPY package.json package-lock.json ./
# 4. npm ci: reproducible y sin dependencias de desarrollo
RUN npm ci --omit=dev && npm cache clean --force
# El código, en último lugar por ser lo más volátil
COPY . .
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]Verifica que ahora los ficheros están donde deben:
Conclusión
Ya sabes fabricar imágenes. docker build no ejecuta nada en tu carpeta: empaqueta el contexto de construcción y se lo envía a BuildKit, que interpreta el Dockerfile como un grafo de pasos y produce capas. Ese punto final del comando es el contexto, no el Dockerfile, y de ahí se derivan tanto el error de COPY fuera del contexto como la necesidad del .dockerignore, que en Aurora Libros ha recortado el envío de 23,41 MB a 47,83 kB y, de paso, ha impedido que un .env con credenciales acabe dentro de la imagen. La opción -f desacopla la ubicación del Dockerfile del contexto, pero las rutas de COPY siguen resolviéndose siempre contra este último.
Has visto también la pieza que más tiempo te va a ahorrar en tu vida diaria: la caché de capas. BuildKit calcula una clave por instrucción a partir de la capa anterior, del texto de la instrucción y del contenido de los ficheros copiados; si esa clave existe, el paso sale CACHED en cero segundos, y si no, se reejecuta ese paso y todos los siguientes. De esa invalidación en cascada nace la regla de oro de ordenar de menos a más volátil, que has medido: 46,7 s frente a 1,4 s ante un simple cambio de código, treinta y tres veces más rápido solo por partir un COPY en dos. Y sabes cuándo desconfiar de la caché con --no-cache y --pull, y cómo leer la salida de BuildKit para localizar el punto exacto en que dejó de haber CACHED.
Lo más importante: auroralibros/aurora-api:0.1.0 existe. Son 167 MB que arrancan Express en un contenedor y responden a curl http://localhost:3000/salud en una máquina donde no hay Node instalado. Cuatro de los quince pasos de onboarding han caído de golpe. Que /libros devuelva ECONNREFUSED no es un fallo: es el siguiente problema del temario, y se resolverá cuando conectes contenedores en red en el módulo 3.
Ahora bien, ese Dockerfile lo has escrito casi a ciegas: sabes qué hace cada línea a grandes rasgos, pero no por qué está escrita así. Por qué npm ci y no npm install, por qué el CMD lleva corchetes y comillas, por qué EXPOSE no expone nada, cuándo usar ADD en lugar de COPY y por qué encadenar comandos con && produce imágenes más pequeñas. Todo eso es la próxima lección, Conceptos Básicos de Dockerfile, donde recorrerás el lenguaje instrucción por instrucción y terminarás con el Dockerfile definitivo de aurora-api, comentado línea a línea y justificado decisión a decisión.
Docker: De Principiante a Avanzado
Módulo 1: Introducción a Docker
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
