Cómo se construye una imagen
Ya podemos leer el Dockerfile de TasteMatch y explicar sus instrucciones. Ahora vamos a observar qué sucede cuando Docker lo procesa. Para esta práctica, utiliza esta versión en la carpeta tastematch, que copia específicamente app.js después de instalar las dependencias:
FROM node:22
WORKDIR /app
COPY package.json .
RUN npm install
COPY app.js .
ENV APP_ENV=production
EXPOSE 3000
CMD ["npm", "start"]
La idea central de esta unidad es:
Docker procesa el Dockerfile paso a paso y puede reutilizar resultados anteriores cuando siguen siendo válidos.
No estudiaremos los componentes internos de Docker. Haremos cambios controlados y utilizaremos la salida del build como evidencia.
Observar un build antes de explicarlo
Desde la carpeta tastematch, construye una nueva imagen:
docker build -t tastematch:3.0 .
No busques todavía una palabra concreta. Observa:
- cuántos pasos aparecen;
- si siguen el orden del Dockerfile;
- cuáles parecen ejecutar trabajo;
- si alguno indica que ha reutilizado un resultado;
- cuánto tarda aproximadamente cada parte.
La presentación cambia entre versiones y configuraciones de Docker. Algunas salidas muestran palabras como CACHED; otras representan los pasos de otra forma. Lo importante es poder relacionar lo observado con FROM, COPY, RUN y las demás instrucciones.
Repite exactamente el mismo comando sin modificar archivos:
docker build -t tastematch:3.0 .
Predice antes de ejecutarlo: si el Dockerfile y sus archivos relevantes no han cambiado, ¿necesita Docker repetir todo el trabajo? Compara ambas salidas y anota qué pasos parecen reutilizarse.
Una imagen se construye por etapas
Podemos representar la construcción con un modelo simplificado:
imagen base node:22
↓
directorio y archivos de preparación
↓
dependencias instaladas
↓
código de TasteMatch
↓
configuración y metadatos
↓
imagen tastematch:3.0
Las imágenes se componen de capas y metadatos. Determinadas instrucciones producen cambios que Docker conserva como resultados de construcción; otras describen configuración. No es correcto convertir este modelo en la regla «cada línea crea siempre una capa».
Para este nivel nos interesa una propiedad práctica: Docker puede trabajar con resultados por etapas. Si una parte sigue siendo válida, puede reutilizarla; si un cambio afecta a una etapa, debe producir nuevos resultados donde corresponda.
resultado anterior válido ──→ reutilizar
resultado afectado ──→ volver a construir
Los resultados no se editan en sitio
Cuando modificamos app.js y volvemos a construir, Docker no abre tastematch:3.0 como si fuera una máquina tradicional para editar un archivo manualmente. Realiza otra construcción.
Durante ella puede aprovechar partes anteriores y generar otras nuevas. La referencia tastematch:3.0 pasa a señalar el resultado de la nueva construcción, pero los contenedores creados anteriormente no se transforman por ello.
archivos modificados
↓
nuevo docker build
↓
reutilización posible + nuevos resultados necesarios
↓
imagen resultante
No necesitamos conocer aquí el sistema de almacenamiento interno. La idea útil es que reconstruir significa producir un resultado, no editar una instalación en marcha.
La caché evita repetir trabajo válido
npm install puede tardar. Si las dependencias no han cambiado, repetir exactamente su instalación en cada build sería trabajo innecesario.
La caché de build permite que Docker reutilice resultados anteriores cuando determina que siguen siendo válidos.
paso de construcción
↓
¿existe un resultado anterior válido?
├── sí → puede reutilizarlo
└── no → vuelve a ejecutarlo
No se trata simplemente de comparar el texto de cada línea. Docker considera la instrucción, los archivos relevantes y otros datos de la construcción. Las reglas exactas pueden variar según la instrucción y la herramienta de build, por lo que observaremos el comportamiento en lugar de memorizar una lista absoluta.
Un experimento de invalidación
Partimos de un build reciente y sin cambios:
docker build -t tastematch:3.0 .
Cambiar solo el código
Modifica únicamente el mensaje de app.js, por ejemplo:
response.send("TasteMatch: observando el build");
Reconstruye:
docker build -t tastematch:3.0 .
Compara la salida con el build anterior:
- ¿se reutilizó la imagen base?;
- ¿volvió a ejecutarse
npm install?; - ¿qué ocurrió a partir de
COPY app.js .?;
No presupongas el resultado: regístralo. Con el Dockerfile actual, separar el archivo de dependencias del código permite que un cambio exclusivo de app.js no afecte necesariamente al paso que instala dependencias.
Cambiar las dependencias
Ahora realiza un cambio real en package.json, como añadir o actualizar conscientemente una dependencia compatible con la práctica. Después reconstruye:
docker build -t tastematch:3.0 .
El archivo utilizado por el primer COPY ha cambiado. Observa qué paso deja de reutilizarse y cuáles deben ejecutarse después.
La conclusión no es una regla textual, sino una relación:
Un cambio relevante puede invalidar un resultado y provocar que los pasos posteriores tengan que volver a realizarse.
El orden puede permitir reutilizar trabajo
Compara dos Dockerfiles funcionales.
Opción A
COPY . .
RUN npm install
Opción B
COPY package.json .
RUN npm install
COPY . .
Si solo cambia app.js, ¿queremos reinstalar las mismas dependencias?
En la opción A, el paso de copia agrupa archivos que cambian por motivos distintos. Un cambio en el código puede afectar al resultado anterior a npm install. En la opción B, el archivo que declara dependencias se incorpora antes y el código después; esto puede permitir reutilizar la instalación cuando package.json no cambia.
La idea general es:
Organiza el Dockerfile para que el trabajo costoso y poco cambiante pueda reutilizarse cuando tenga sentido.
No significa «copia siempre package.json primero». Otros lenguajes y proyectos tienen otros archivos y necesidades. Un orden correcto debe mantener la construcción clara y producir la imagen adecuada; medir y observar es mejor que aplicar una receta universal.
Variante: cuando el proyecto ya incluye un lockfile
Muchos proyectos Node incluyen package-lock.json, que registra las versiones resueltas de sus dependencias. Si el proyecto ya dispone de un lockfile coherente con package.json, podemos copiar ambos y utilizar npm ci:
COPY package.json package-lock.json ./
RUN npm ci
npm ci instala de acuerdo con ese lockfile y falla si no es coherente con package.json. Es una variante útil para proyectos que ya entregan ambos archivos; no necesitamos aprender a generar el lockfile en esta unidad.
Para el recorrido principal de TasteMatch conservaremos COPY package.json . y RUN npm install. npm se ejecuta dentro del build: no necesitas instalar Node.js ni npm en el host.
El punto es el contexto de build
Volvamos al comando:
docker build -t tastematch:3.0 .
El punto final indica el contexto de build. El contexto define el conjunto de archivos que Docker puede utilizar durante la construcción.
No confundas tres lugares diferentes:
| Concepto | Qué representa |
|---|---|
| Contexto de build | Archivos puestos a disposición de la construcción. |
WORKDIR /app | Directorio de trabajo dentro de la imagen y del contenedor. |
| Contenido de la imagen | Archivos incorporados por las instrucciones del Dockerfile. |
carpeta del host usada como contexto
≠
/app dentro de la imagen
≠
todo el contenido final de la imagen
Un archivo puede estar disponible en el contexto y no terminar en la imagen. Para incorporarlo debe intervenir una instrucción como COPY o ADD.
¿Necesitamos todo lo que hay en el proyecto?
Durante el trabajo puede aparecer una estructura como esta:
tastematch/
├── Dockerfile
├── package.json
├── app.js
├── node_modules/
├── .git/
├── .env
└── debug.log
Pregúntate qué necesita realmente el build. En esta práctica:
package.jsondeclara dependencias;app.jscontiene la aplicación;Dockerfilecontiene las instrucciones;node_modules/del host no debe sustituir la instalación realizada en la imagen;.git/contiene historial del repositorio, no la aplicación;.envpuede contener configuración local o sensible;debug.loges un resultado local de diagnóstico.
Un contexto innecesariamente grande puede transferir o poner a disposición archivos que no hacen falta, hacer menos eficiente el build, influir en la reutilización y aumentar el riesgo de trabajar accidentalmente con archivos sensibles.
Esto no significa que todo el contexto se copie automáticamente. Lo que acaba en la imagen depende también del Dockerfile.
Limitar el contexto con .dockerignore
Creamos .dockerignore en la raíz del contexto para excluir elementos que este build no necesita:
node_modules
.git
.env
*.log
Cada línea responde a una decisión concreta:
| Patrón | Motivo en TasteMatch |
|---|---|
node_modules | Las dependencias se instalan dentro de la imagen mediante npm install. |
.git | El historial no es necesario para construir o ejecutar la aplicación. |
.env | La configuración local no debe entrar accidentalmente en este contexto. |
*.log | Los logs locales no forman parte de la aplicación. |
.dockerignore limita qué archivos del contexto se ponen a disposición del build. No borra archivos de una imagen existente y no sustituye a las instrucciones COPY.
Tampoco es un gestor de secretos. Excluir .env reduce el riesgo de que quede disponible accidentalmente durante el build, pero no protege un secreto que ya se publicó, se copió por otra vía o se incluyó en una imagen anterior.
Comprobar nuestra decisión
- Examina los archivos del proyecto.
- Justifica cuáles no necesita la construcción.
- Crea
.dockerignorecon los patrones correspondientes. - Construye de nuevo:
docker build -t tastematch:3.0 .
- Ejecuta un contenedor de comprobación:
docker run -d --name tastematch-3-0 -p 8080:3000 tastematch:3.0
docker logs tastematch-3-0
- Visita http://localhost:8080 y confirma que TasteMatch sigue funcionando.
- Retira únicamente el contenedor de esta práctica:
docker stop tastematch-3-0
docker rm tastematch-3-0
Si la aplicación deja de funcionar, no añadas o retires patrones al azar: comprueba si excluiste un archivo que el Dockerfile necesita.
Un Dockerfile mejor organizado
Con el contexto razonado, el Dockerfile queda así:
FROM node:22
WORKDIR /app
COPY package.json .
RUN npm install
COPY app.js .
ENV APP_ENV=production
EXPOSE 3000
CMD ["npm", "start"]
Y .dockerignore:
node_modules
.git
.env
*.log
Los cambios resuelven problemas concretos:
- los archivos de dependencias se incorporan antes del código para separar cambios distintos;
COPY app.js .expresa exactamente qué código necesita esta aplicación mínima;.dockerignoremantiene fuera del contexto los elementos identificados como innecesarios.
Este Dockerfile está mejor organizado para nuestro experimento. No afirmamos que sea óptimo para producción ni una plantilla universal.
Observar el historial de la imagen
Ejecuta:
docker history tastematch:3.0
El comando muestra información del historial de la imagen, como instrucciones asociadas y tamaños orientativos. Relaciona las filas que reconozcas con el Dockerfile, pero no intentes interpretar ahora todas las columnas.
docker history es una herramienta de observación, no una representación perfecta de todos los detalles internos del build. Úsala para formular preguntas: ¿qué preparación aparece?, ¿qué elementos parecen aportar tamaño?, ¿reconoces el comando de inicio?
También puedes consultar:
docker image ls tastematch
El tamaño depende, entre otros factores, de la imagen base, el software instalado y los archivos incorporados. No compararemos todavía familias de imágenes base ni aplicaremos técnicas avanzadas de reducción.
Experimento completo de caché
Utiliza el Dockerfile organizado y .dockerignore. Guarda primero el estado actual de package.json y app.js para poder identificar cada cambio.
1. Build inicial
docker build -t cache-demo:1.0 .
Registra qué pasos realizan trabajo.
2. Repetir sin cambios
docker build -t cache-demo:1.0 .
Compara la salida y anota qué resultados puede reutilizar Docker. No dependas de que aparezca una palabra exacta.
3. Modificar solo código
Cambia únicamente el mensaje de app.js y reconstruye:
docker build -t cache-demo:1.0 .
Observa si npm install vuelve a ejecutarse y qué ocurre a partir de COPY app.js ..
4. Modificar dependencias
Realiza un cambio controlado en las dependencias de package.json. Por ejemplo, añade "ms": "2.1.3" a dependencies, conservando Express; si ya la añadiste en el experimento anterior, retírala ahora. No necesitas utilizarla en el código: observamos qué sucede al cambiar la instalación. Después reconstruye:
docker build -t cache-demo:1.0 .
Observa el primer paso afectado y los posteriores.
5. Comparar
Responde con evidencias de las tres salidas:
- ¿Qué se reutilizó al repetir sin cambios?
- ¿Qué volvió a realizarse al cambiar solo
app.js? - ¿Cuándo se repitió
npm install? - ¿Qué archivo hizo relevante ese cambio?
- ¿Cómo influyó el orden del Dockerfile?
Errores habituales
Copiar todo antes de instalar sin razonar
COPY . .
RUN npm install
Puede funcionar, pero mezcla cambios frecuentes del código con los archivos de dependencias. Sepáralos cuando eso sea correcto para el proyecto y permita reutilizar trabajo costoso.
Pensar que .dockerignore modifica una imagen construida
.dockerignore actúa sobre el contexto de una construcción. No elimina retrospectivamente archivos de tastematch:2.0 ni de un contenedor existente.
Confundir contexto con WORKDIR
El contexto pertenece a la entrada del build desde el host. WORKDIR declara un directorio dentro de la imagen. Que ambos utilicen un punto en algunos comandos no los convierte en el mismo lugar.
Pensar que todo el contexto termina en la imagen
El contexto hace que los archivos estén disponibles para construir. Solo se incorporan cuando las instrucciones correspondientes los utilizan.
Utilizar --no-cache como primera respuesta
Forzar un build sin caché puede ser útil en una investigación concreta, pero no explica qué resultado se estaba reutilizando ni por qué. Primero observa el paso afectado, los archivos que utiliza y los cambios realizados.
Optimizar antes de comprender
Un Dockerfile correcto y legible es mejor que una colección de trucos. Optimiza un problema observado y conserva la capacidad de explicar cada decisión.
Mini reto: investiga un build
Descarga el starter del mini reto (ZIP) y descomprímelo. Contiene la aplicación y dos pares coherentes de package.json y package-lock.json: uno inicial y otro en cambio-dependencias/ para el experimento. No necesitas generar ni editar el lockfile.
build-reto/
├── package.json
├── package-lock.json
├── src/
│ └── server.js
├── cambio-dependencias/
│ ├── package.json
│ └── package-lock.json
└── README-starter.md
El package.json inicial contiene:
{
"name": "build-reto",
"version": "1.0.0",
"private": true,
"scripts": { "start": "node src/server.js" },
"dependencies": { "express": "5.1.0" }
}
Y src/server.js:
const express = require("express");
const app = express();
app.get("/", (_request, response) => {
response.send("Build reto preparado");
});
app.listen(3000, "0.0.0.0", () => {
console.log("Build reto escucha en el puerto 3000");
});
Para practicar las exclusiones, crea con el editor un .env con APP_MODE=practice y un debug.log con una línea de prueba, sin secretos. node_modules/ y .git/ son ejemplos de directorios locales que podrían aparecer en un proyecto; no se entregan ni necesitas crearlos para resolver el reto. Decide también si cambio-dependencias/ y el README son necesarios en el contexto.
Crea tú el archivo Dockerfile en la raíz de build-reto. Su contenido inicial es:
FROM node:22
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]
No recibes el Dockerfile final. Debes justificar tus decisiones:
- identifica qué archivos necesita la construcción y cuáles no;
- crea
.dockerignoreexplicando cada patrón; - decide cómo incorporar los archivos de dependencias y el código;
- decide si el lockfile permite utilizar
npm ci; - reorganiza el Dockerfile sin aplicar una plantilla a ciegas;
- construye
build-reto:1.0; - repite el build sin cambios y registra la reutilización;
- modifica únicamente
src/server.jsy reconstruye; - sustituye los dos archivos de dependencias de la raíz por los de
cambio-dependencias/, que añadenmssin cambiar el código, y reconstruye; - compara cuándo se ejecutó la instalación;
- utiliza
docker history build-reto:1.0como observación adicional; - ejecuta la aplicación publicando su puerto interno 3000 en un puerto libre del host y comprueba que
/respondeBuild reto preparado(o el mensaje que hayas cambiado); retira después únicamente el contenedor del reto.
Entrega el Dockerfile y .dockerignore resultantes, las tres observaciones del build y una explicación de cómo el orden responde a los cambios del proyecto.
De escribir instrucciones a razonar la construcción
Ahora puedes mirar un build como una secuencia de resultados relacionados:
Dockerfile + contexto
↓
procesamiento por etapas
↓
resultados reutilizables cuando siguen siendo válidos
↓
imagen resultante
También puedes distinguir claramente:
contexto de build ≠ WORKDIR ≠ contenido final de la imagen
La siguiente necesidad aparece cuando la aplicación genera o modifica datos que deben sobrevivir a sus contenedores. En la próxima unidad estudiaremos esa persistencia; aquí dejamos cerrada la construcción de la imagen sin adelantar volúmenes ni montajes.