Crear nuestra primera imagen
Hasta ahora hemos partido de imágenes que ya estaban preparadas. Con Nginx, el recorrido comenzaba así:
Imagen existente
↓
docker run
↓
Contenedor
Pero TasteMatch es nuestra aplicación. Su código no aparece por arte de magia dentro de una imagen de Nginx ni de Node.js. Necesitamos preparar una imagen que contenga lo necesario para ejecutarla.
En esta unidad completaremos por primera vez este recorrido:
Nuestra aplicación
↓
Dockerfile
↓
docker build
↓
Nuestra imagen
↓
docker run
↓
Nuestro contenedor
No intentaremos aprender todas las posibilidades de un Dockerfile. Primero construiremos una imagen pequeña y comprobaremos qué ocurre en cada paso.
La aplicación que queremos contenerizar
Utilizaremos una versión mínima de TasteMatch. Solo mostrará un mensaje en el navegador, pero tendrá las mismas clases de necesidades que una aplicación mayor: código, un runtime, una dependencia y un comando de inicio.
Crea una carpeta llamada tastematch y entra en ella:
mkdir tastematch
cd tastematch
Dentro, crea package.json:
{
"name": "tastematch",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "5.1.0"
}
}
Este archivo describe el proyecto, su dependencia y el comando npm start. No necesitas conocer Express para seguir la práctica.
Crea también app.js:
const express = require("express");
const app = express();
const port = 3000;
app.get("/", (_request, response) => {
response.send("TasteMatch: hola desde Docker");
});
app.listen(port, "0.0.0.0", () => {
console.log(`TasteMatch escucha en el puerto ${port}`);
});
El código inicia un servicio HTTP en el puerto 3000. Escucha en 0.0.0.0 para aceptar las conexiones que lleguen a las interfaces del contenedor. Nuestro objetivo no es estudiar este código, sino preparar el entorno en el que pueda ejecutarse.
La carpeta debe tener por ahora esta forma:
tastematch/
├── app.js
└── package.json
Antes del Dockerfile: analizar la aplicación
No escribas instrucciones todavía. Primero responde con la información que acabamos de obtener:
- ¿Qué runtime necesita? Node.js.
- ¿Qué archivos necesita?
app.jsypackage.json. - ¿Tiene dependencias? Sí, Express, declarada en
package.json. - ¿Hay que preparar algo? Instalar las dependencias con
npm install. - ¿Qué comando la inicia?
npm start.
Este análisis nos da un plan:
TasteMatch
├── partir de un entorno con Node.js
├── elegir dónde estará la aplicación
├── introducir sus archivos
├── instalar su dependencia
└── indicar cómo se inicia
Queremos que Docker pueda repetir ese plan sin depender de que alguien recuerde una lista de pasos.
Describir la imagen con un Dockerfile
Un Dockerfile es un archivo de texto con instrucciones que Docker utiliza para construir una imagen. Normalmente se llama exactamente Dockerfile, sin extensión.
Lo construiremos poco a poco. Cada instrucción responderá a una necesidad que ya hemos identificado.
Partir de un entorno con Node.js: FROM
TasteMatch necesita Node.js. En lugar de preparar un sistema completamente desde cero, partiremos de una imagen que ya lo contiene:
FROM node:22
FROM indica la imagen base. Nuestra imagen será nueva, pero aprovechará una base existente que aporta Node.js y npm.
Imagen node:22
↓
añadimos TasteMatch
↓
Nuestra imagen
En esta primera construcción utilizaremos node:22 sin comparar variantes ni optimizar su tamaño.
Elegir el directorio de la aplicación: WORKDIR
Necesitamos un lugar dentro de la imagen para trabajar con los archivos de TasteMatch:
WORKDIR /app
WORKDIR establece /app como directorio de trabajo para las instrucciones siguientes. Cuando usemos rutas relativas, partirán de ese directorio.
Imagen
└── /app ← directorio de trabajo
Introducir nuestros archivos: COPY
La imagen base contiene Node.js, pero no conoce app.js ni package.json. Tenemos que copiarlos:
COPY . .
En esta instrucción:
- el primer
.representa los archivos disponibles desde la ubicación que usaremos para construir; - el segundo
.representa el destino actual dentro de la imagen,/appgracias aWORKDIR.
Por tanto, nuestros archivos quedarán dentro de /app. Más adelante estudiaremos con precisión qué archivos participan en una construcción; ahora solo necesitamos ejecutar el build desde la carpeta tastematch.
Instalar la dependencia: RUN
Copiar package.json no instala Express. Durante la construcción necesitamos ejecutar:
RUN npm install
RUN ejecuta un comando mientras Docker construye la imagen. El resultado de npm install queda preparado en la imagen que estamos creando.
Todavía no estamos iniciando TasteMatch:
docker build
↓
RUN npm install
↓
dependencias preparadas en la imagen
Indicar cómo se inicia: CMD
La imagen también debe indicar qué comando queremos utilizar por defecto al iniciar un contenedor:
CMD ["npm", "start"]
CMD no inicia la aplicación durante el build. Se utilizará después, cuando creemos un contenedor a partir de la imagen.
RUN npm install CMD ["npm", "start"]
↓ ↓
construcción de la imagen inicio del contenedor
Por ahora basta con conservar esta diferencia: RUN prepara la imagen; CMD proporciona el comando de inicio del contenedor.
El primer Dockerfile completo
Ahora sí, crea un archivo llamado Dockerfile dentro de tastematch:
FROM node:22
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]
Puedes explicar cada línea a partir del análisis anterior:
| Instrucción | Necesidad que resuelve |
|---|---|
FROM node:22 | Partir de un entorno que contiene Node.js y npm. |
WORKDIR /app | Establecer el directorio de trabajo de TasteMatch. |
COPY . . | Introducir los archivos de la aplicación. |
RUN npm install | Instalar su dependencia durante la construcción. |
CMD ["npm", "start"] | Indicar el comando por defecto al iniciar un contenedor. |
Este Dockerfile responde a las necesidades concretas de esta aplicación. No es una plantilla universal para cualquier proyecto.
La carpeta ya está preparada:
tastematch/
├── Dockerfile
├── app.js
└── package.json
Construir la imagen
Comprueba que la terminal se encuentra dentro de tastematch y ejecuta:
docker build -t tastematch:1.0 .
Lee el comando de izquierda a derecha:
docker build
→ construir una imagen
-t tastematch:1.0
→ asignarle el nombre tastematch y el tag 1.0
.
→ utilizar como contexto la carpeta actual
El punto final importa. Para esta práctica significa que Docker puede utilizar los archivos de la carpeta actual durante la construcción. El contexto de build se estudiará con más detalle en otra unidad.
Observar antes de continuar
No ignores la salida del build. Busca pasos relacionados con:
FROM
↓
WORKDIR
↓
COPY
↓
RUN npm install
↓
CMD
La presentación exacta puede variar, pero deberías poder relacionar lo que Docker procesa con las instrucciones del archivo. Al terminar debe aparecer un resultado satisfactorio y la referencia tastematch:1.0.
No hemos iniciado la aplicación. docker build ha producido una imagen.
Comprobar la imagen
Utiliza el comando que ya conoces de Docker 0:
docker image ls
Localiza una fila con estos valores:
REPOSITORY TAG
tastematch 1.0
La salida incluye más columnas y sus valores pueden variar. Antes de seguir, responde:
- ¿Tenemos una imagen o un contenedor?
- ¿Está TasteMatch ejecutándose por haber completado el build?
- ¿Qué operación conocida crea un contenedor a partir de una imagen?
Tenemos una imagen que todavía no se está ejecutando. El siguiente paso es docker run.
Ejecutar nuestra propia imagen
TasteMatch escucha en el puerto 3000 del contenedor. Reutilizaremos la publicación de puertos aprendida en Docker 0 y utilizaremos el 3000 del host:
docker run -d --name tastematch-1-0 -p 3000:3000 tastematch:1.0
Ya conoces las piezas de docker run: creamos tastematch-1-0 desde nuestra imagen y publicamos el puerto del servicio. Comprueba el estado y los mensajes de la aplicación:
docker ps
docker logs tastematch-1-0
En los logs debe aparecer:
TasteMatch escucha en el puerto 3000
Abre http://localhost:3000. La respuesta esperada es:
TasteMatch: hola desde Docker
Acabas de recorrer el camino completo:
app.js + package.json
↓
Dockerfile
↓
docker build -t tastematch:1.0 .
↓
imagen tastematch:1.0
↓
docker run
↓
contenedor tastematch-1-0
↓
TasteMatch en el navegador
¿Qué ocurre si cambia el código?
La imagen tastematch:1.0 contiene los archivos que se incorporaron durante su construcción. Vamos a cambiar el archivo original del host.
En app.js, sustituye:
response.send("TasteMatch: hola desde Docker");
por:
response.send("TasteMatch: mi primera aplicación contenerizada");
Antes de ejecutar otro comando, haz una predicción:
Si recargamos ahora el navegador, ¿ha cambiado automáticamente la imagen
tastematch:1.0o el contenedor que ya está en marcha?
Recarga http://localhost:3000. El contenedor continúa mostrando el mensaje anterior.
app.js del host imagen tastematch:1.0
ya contiene el mensaje nuevo conserva los archivos construidos
≠
Modificar el código fuente del host no modifica una imagen existente. Tampoco sustituye los archivos del contenedor que ya creamos desde ella.
Construir la versión 1.1
Para incorporar el cambio, vuelve a construir y utiliza otro tag:
docker build -t tastematch:1.1 .
Comprueba las imágenes:
docker image ls tastematch
Ahora deben aparecer ambos tags:
REPOSITORY TAG
tastematch 1.1
tastematch 1.0
Los IDs y el orden pueden variar. En esta práctica, 1.0 identifica la construcción con el mensaje anterior y 1.1 la construcción que incorpora el nuevo. Todavía no estamos definiendo una estrategia general de versionado.
Deja el primer contenedor en el puerto 3000 y crea otro desde 1.1 utilizando un puerto libre diferente del host:
docker run -d --name tastematch-1-1 -p 3001:3000 tastematch:1.1
Comprueba:
docker ps
docker logs tastematch-1-1
Visita las dos direcciones:
| Dirección | Imagen del contenedor | Respuesta esperada |
|---|---|---|
| http://localhost:3000 | tastematch:1.0 | TasteMatch: hola desde Docker |
| http://localhost:3001 | tastematch:1.1 | TasteMatch: mi primera aplicación contenerizada |
La comparación demuestra que el código del host, las imágenes construidas y los contenedores creados a partir de ellas son elementos distintos.
Cuando termines la práctica, retira solo sus contenedores:
docker stop tastematch-1-0 tastematch-1-1
docker rm tastematch-1-0 tastematch-1-1
docker ps -a
docker image ls tastematch
Los contenedores desaparecen y las dos imágenes permanecen disponibles.
Si algo falla, busca dónde se rompe el recorrido
Docker no encuentra los archivos esperados
El . de este comando se refiere a la carpeta actual:
docker build -t tastematch:1.0 .
Si lo ejecutas desde otra carpeta, Docker utilizará otro contexto y puede no encontrar los archivos de TasteMatch. Entra en tastematch, comprueba su contenido y repite el comando desde allí.
Docker no encuentra el Dockerfile
Por defecto, el build busca un archivo llamado exactamente Dockerfile en la ubicación correspondiente. Comprueba el nombre, incluidas mayúsculas y ausencia de extensión.
Confundes construir con ejecutar
Relaciona cada operación con su resultado:
docker build → crea una imagen
docker run → crea e inicia un contenedor
Que el build termine correctamente no significa que exista un contenedor en ejecución.
Esperas que RUN se ejecute al iniciar
En nuestro Dockerfile:
RUN npm install → ocurre durante docker build
CMD ["npm", "start"] → se utiliza con docker run
Aquí RUN prepara las dependencias; no es el comando que mantiene el contenedor ejecutándose.
Modificas el código, pero ves la respuesta anterior
Una imagen ya construida no cambia cuando editas el archivo original. Construye una nueva imagen e inicia un contenedor desde el tag correcto. Comprueba también qué imagen utiliza cada contenedor.
El build funciona, pero la web no responde
Recupera el diagnóstico que ya practicaste en Docker 0:
- comprueba con
docker ps -aque el contenedor existe y está activo; - consulta sus mensajes con
docker logs NOMBRE; - revisa en
docker psla publicación del puerto; - utiliza en el navegador el puerto del host que elegiste;
- si el puerto está ocupado, retira únicamente tu intento fallido y elige otro libre.
No necesitas cambiar el Dockerfile a ciegas para resolver un problema de estado, logs o publicación.
Mini reto: conteneriza una aplicación preparada
Recibes una aplicación llamada saludo-web que ya funciona fuera de Docker después de instalar sus dependencias:
saludo-web/
├── package.json
└── server.js
package.json contiene:
{
"name": "saludo-web",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "5.1.0"
}
}
server.js contiene:
const express = require("express");
const app = express();
app.get("/", (_request, response) => {
response.send("Saludo web: primera versión");
});
app.listen(4000, "0.0.0.0", () => {
console.log("Saludo web escucha en el puerto 4000");
});
Tu objetivo es completar el recorrido sin copiar el Dockerfile de TasteMatch como una receta. Primero responde:
- ¿qué runtime necesita?;
- ¿qué archivos necesita?;
- ¿qué dependencia hay que preparar?;
- ¿qué comando instala esa dependencia?;
- ¿qué comando inicia la aplicación?;
- ¿en qué puerto escucha dentro del contenedor?
Después:
- crea un
Dockerfilerazonado que utiliceFROM,WORKDIR,COPY,RUNyCMD; - construye
saludo-web:1.0y comprueba que la imagen existe; - crea un contenedor con un nombre reconocible y publica el servicio en un puerto libre del host;
- comprueba el estado, los logs y la respuesta HTTP;
- cambia el mensaje a
Saludo web: segunda versión; - predice qué mostrará el primer contenedor antes de volver a construir;
- construye
saludo-web:1.1; - crea otro contenedor sin provocar un conflicto de puertos;
- demuestra qué mensaje sirve cada contenedor;
- explica por qué el primero no cambió al editar
server.jsni al construir1.1; - retira los contenedores del reto y comprueba el resultado.
Entrega los comandos que elegiste y una evidencia breve de cada comprobación. Tu explicación final debe recorrer:
Aplicación
↓
Dockerfile
↓
docker build
↓
Imagen propia
↓
docker run
↓
Contenedor
De una primera imagen a comprender el Dockerfile
Ya no dependemos únicamente de imágenes preparadas por otras personas. Hemos analizado una aplicación, descrito cómo preparar su entorno, construido dos versiones de nuestra imagen y creado contenedores a partir de ellas.
También hemos comprobado una diferencia fundamental:
editar el código del host
≠
modificar una imagen ya construida
En la siguiente unidad volveremos a este Dockerfile para entender mejor sus instrucciones y tomar decisiones con más criterio. Nuestro objetivo dejará de ser simplemente conseguir que funcione: podremos explicar con más profundidad qué expresa cada línea.