Saltar al contenido principal

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.js y package.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, /app gracias a WORKDIR.

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ónNecesidad que resuelve
FROM node:22Partir de un entorno que contiene Node.js y npm.
WORKDIR /appEstablecer el directorio de trabajo de TasteMatch.
COPY . .Introducir los archivos de la aplicación.
RUN npm installInstalar 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:

  1. ¿Tenemos una imagen o un contenedor?
  2. ¿Está TasteMatch ejecutándose por haber completado el build?
  3. ¿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.0 o 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ónImagen del contenedorRespuesta esperada
http://localhost:3000tastematch:1.0TasteMatch: hola desde Docker
http://localhost:3001tastematch:1.1TasteMatch: 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:

  1. comprueba con docker ps -a que el contenedor existe y está activo;
  2. consulta sus mensajes con docker logs NOMBRE;
  3. revisa en docker ps la publicación del puerto;
  4. utiliza en el navegador el puerto del host que elegiste;
  5. 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:

  1. crea un Dockerfile razonado que utilice FROM, WORKDIR, COPY, RUN y CMD;
  2. construye saludo-web:1.0 y comprueba que la imagen existe;
  3. crea un contenedor con un nombre reconocible y publica el servicio en un puerto libre del host;
  4. comprueba el estado, los logs y la respuesta HTTP;
  5. cambia el mensaje a Saludo web: segunda versión;
  6. predice qué mostrará el primer contenedor antes de volver a construir;
  7. construye saludo-web:1.1;
  8. crea otro contenedor sin provocar un conflicto de puertos;
  9. demuestra qué mensaje sirve cada contenedor;
  10. explica por qué el primero no cambió al editar server.js ni al construir 1.1;
  11. 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.