Contenido

Tutorial de Dockerfile: empaqueta tu programa en una imagen

Dockerfile es un archivo de texto sin formato que indica, línea por línea, «cómo crear esta imagen». Docker ejecuta estos pasos y genera una imagen que luego puedes iniciar con docker run, igual que nginx.

Proyecto de ejemplo

Crea la carpeta hello-node y añade estos dos archivos.

package.json:

{
  "name": "hello-node",
  "version": "1.0.0",
  "scripts": { "start": "node server.js" }
}

server.js:

const http = require("http");

const server = http.createServer((req, res) => {
  res.end("Hello from Docker!\n");
});

server.listen(3000, () => console.log("Listening on port 3000"));

No necesitas instalar Node.js en tu ordenador. Node.js estará en la imagen; para eso sirve Docker.

Escribe el Dockerfile

Crea un archivo llamado Dockerfile (sin extensión) en la misma carpeta:

# 1. Usa como base la imagen oficial de Node.js
FROM node:24-alpine

# 2. Establece el directorio de trabajo dentro del contenedor
WORKDIR /app

# 3. Copia primero la lista de paquetes e instala las dependencias
COPY package*.json ./
RUN npm install --omit=dev

# 4. Copia después el resto del código
COPY . .

# 5. Indica el puerto que usará el contenedor
EXPOSE 3000

# 6. Comando que se ejecuta al iniciar el contenedor
CMD ["npm", "start"]

Ahora hay tres archivos en la carpeta:

$ ls
Dockerfile
package.json
server.js

Construir la imagen: docker build

docker build -t hello-node .

-t hello-node es el nombre de la imagen. El . final indica que se use la carpeta actual como contenido de compilación (build context). Docker envía los archivos de esta carpeta al motor de compilación, y la instrucción COPY solo puede copiar archivos que estén dentro del contenido de compilación.

Durante la compilación, la terminal muestra el progreso en tiempo real. Al terminar, la salida se parece a esta:

$ docker build -t hello-node .

[+] Building 9.9s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 399B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              1.9s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 2B                                                                0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  3.6s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => sha256:7ce7dcbafb7f750cf0c6be3c30e34372b06dd1182fa67f6ae30782c8150c5444 0B / 444B       7.9s
 => => sha256:02f40d53b06f6b91fec0cd6a6719656da99270bff8ee54ed8dcdf4c157763fc 1.26MB / 1.26MB  0.7s
 => => sha256:3c504d5a797be28f9de43147d49cd8ecb2eea46dfaf7beb8715db695d5f0a 56.52MB / 56.52MB  1.3s
 => => sha256:e2de96513ba9eb53b431787ec8a65cdde380ac4772a3e4c4b714dcfde2a102b 3.85MB / 3.85MB  0.6s
 => => extracting sha256:e2de96513ba9eb53b431787ec8a65cdde380ac4772a3e4c4b714dcfde2a102b5      0.6s
 => => extracting sha256:3c504d5a797be28f9de43147d49cd8ecb2eea46dfaf7beb8715db695d5f0a7f0      2.2s
 => => extracting sha256:02f40d53b06f6b91fec0cd6a6719656da99270bff8ee54ed8dcdf4c157763fc1      0.1s
 => => extracting sha256:7ce7dcbafb7f750cf0c6be3c30e34372b06dd1182fa67f6ae30782c8150c5444      0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 759B                                                              0.0s
 => [2/5] WORKDIR /app                                                                         0.0s
 => [3/5] COPY package*.json ./                                                                0.0s
 => [4/5] RUN npm install --omit=dev                                                           1.4s
 => [5/5] COPY . .                                                                             0.0s
 => exporting to image                                                                         2.7s
 => => exporting layers                                                                        0.2s
 => => exporting manifest sha256:b311d231396fab904d11e3fbac8bade8302e8e7401fe14ba1e4a736f4fed  0.0s
 => => exporting config sha256:fa1ad1088d08bf3f3f035dfe9dbed3cf5a008492316926ba23fae3e58d6852  0.0s
 => => exporting attestation manifest sha256:0ea092cede0047cad91ad77043f79ef84c7cab9153913acf  0.0s
 => => exporting manifest list sha256:62664c2b6134a7c365eacb28019730c69b7d1d0fd855c559f6994c3  0.0s
 => => naming to docker.io/library/hello-node:latest                                           0.0s
 => => unpacking to docker.io/library/hello-node:latest                                        2.5s

Cada línea que empieza con => es un paso:

PasoInstrucción Dockerfile correspondienteDescripción
load build definition—Lee el Dockerfile
load metadata for …node:24-alpineFROMConsulta la versión más reciente de la imagen base
[1/5] FROM …FROM node:24-alpineDescarga la imagen base (los sha256:… de abajo muestran el progreso de descarga de cada capa; la primera vez tarda más)
load build context—Envía el contenido de compilación (esta vez solo 759B, porque hay solo 3 archivos pequeños)
[2/5] WORKDIR /appWORKDIRCrea /app y cambia a esa carpeta
[3/5] COPY package*.json ./COPYCopia el manifiesto de paquetes
[4/5] RUN npm install --omit=devRUNInstala los paquetes en la imagen
[5/5] COPY . .COPYCopia el resto del código
exporting to image—Guarda el resultado como imagen y le asigna el nombre hello-node:latest

EXPOSE y CMD solo configuran opciones; no generan pasos de compilación.

Ejecutar

$ docker images hello-node
IMAGE               ID             DISK USAGE   CONTENT SIZE   EXTRA
hello-node:latest   62664c2b6134        245MB         62.2MB

$ docker run -d -p 3000:3000 --name hello hello-node
2c4745bd0187d9ba7f0e9af096519ce9a2ad4436395490f5af24cd142f3bef4c

$ curl -s http://localhost:3000
Hello from Docker!

$ docker logs hello

> hello-node@1.0.0 start
> node server.js

Listening on port 3000
El navegador muestra Hello from Docker!
Abre http://localhost:3000 en el navegador
  • La imagen ocupa unos 245 MB; la mayor parte corresponde al propio Node.js (node:24-alpine).
  • En docker logs, > node server.js es un mensaje que imprime npm start. A continuación aparece Listening on port 3000, que sí procede del programa.

Reconstruir después de modificar el código: caché

Cambia el texto de server.js por Hello from Docker! (v2) y vuelve a construir la imagen:

$ docker build -t hello-node .

[+] Building 0.4s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 399B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.3s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 2B                                                                0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 294B                                                              0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => CACHED [3/5] COPY package*.json ./                                                         0.0s
 => CACHED [4/5] RUN npm install --omit=dev                                                    0.0s
 => [5/5] COPY . .                                                                             0.0s
 => exporting to image                                                                         0.1s
 => => exporting layers                                                                        0.0s
 => => exporting manifest sha256:667a883ab027b5db474215519a69eef0b162988f9f6384d75c7e6e819ebc  0.0s
 => => exporting config sha256:c3795d6af309587aa9f021692bf628df45d1d38a5e12d05388447644423a01  0.0s
 => => exporting attestation manifest sha256:4598cd1b9ff7979933f7bc06c48df78bb906278618ad587a  0.0s
 => => exporting manifest list sha256:fb09b954543fd2fa9a8d93763672d6a0b8d3ac26e7751e3b3f69b79  0.0s
 => => naming to docker.io/library/hello-node:latest                                           0.0s
 => => unpacking to docker.io/library/hello-node:latest                                        0.0s

Esta vez solo tardó 0.4 segundos:

  • [2/5], [3/5] y [4/5] aparecen marcados como CACHED: estos pasos son exactamente iguales que la vez anterior, así que se usan los resultados anteriores y no hace falta volver a ejecutar npm install.
  • Solo se volvió a ejecutar [5/5] COPY . ., porque se modificó server.js.
$ docker run --rm -d -p 3000:3000 --name hello hello-node
9d4e4fc490b7b2e5d4c4c5ba729359c63271cbfa7934f6fff011a4fd4d6e87ca

$ curl -s http://localhost:3000
Hello from Docker! (v2)

$ docker stop hello
hello

Por qué copiar primero package.json

Cada línea del Dockerfile genera una capa, y Docker las guarda en caché. Cuando cambia una línea (o los archivos que copia), hay que volver a ejecutar todos los pasos posteriores.

Si ejecutas COPY . . al principio, la caché de npm install se invalida cada vez que cambias una línea de código, y tendrás que volver a instalar todos los paquetes. Si primero copias solo package*.json y después instalas, solo tendrás que reinstalar cuando cambie la lista de paquetes. Cuantos más paquetes tenga el proyecto, mayor será la diferencia. Consulta la comparación detallada en Caché de compilación y optimización.

.dockerignore: no envíes archivos innecesarios durante la compilación

Al desarrollar en tu equipo, la carpeta suele contener muchos archivos, como node_modules, .git y archivos de registro. Para demostrarlo, coloca un node_modules de 30 MB en la carpeta y vuelve a crear la imagen; fíjate en la línea load build context:

$ docker build -t hello-node .

[+] Building 2.0s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 399B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.2s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 2B                                                                0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.4s
 => => transferring context: 30.01MB                                                           0.3s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => CACHED [3/5] COPY package*.json ./                                                         0.0s
 => CACHED [4/5] RUN npm install --omit=dev                                                    0.0s
 => [5/5] COPY . .                                                                             0.1s
 => exporting to image                                                                         1.3s
 => => exporting layers                                                                        1.0s
 => => exporting manifest sha256:5c28f64104e51f64822405ae7a67a9016f62e9c5a4c8425b5b92c47791af  0.0s
 => => exporting config sha256:21b97fa4b8887f00cdf80b0dbeb6658f8dd678b8c4008f8d1dcb5c191e82ba  0.0s
 => => exporting attestation manifest sha256:bc9366f473fd5bb6fdffec42cdcd8155bd12b28cfc6ac23d  0.0s
 => => exporting manifest list sha256:f36d091892aca26420e44f681f5279c82f8881c32eb6d77978d99ee  0.0s
 => => naming to docker.io/library/hello-node:latest                                           0.0s
 => => unpacking to docker.io/library/hello-node:latest                                        0.2s

transferring context: 30.01MB: se envía todo node_modules al motor de compilación y, además, COPY . . lo copia a la imagen, sobrescribiendo la versión instalada allí con npm install.

Crea .dockerignore y enumera los archivos que no quieras enviar a la compilación:

node_modules
.git
.env
*.log

Vuelve a crear la imagen:

$ docker build -t hello-node .

[+] Building 0.4s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 399B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.3s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 69B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 159B                                                              0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => CACHED [3/5] COPY package*.json ./                                                         0.0s
 => CACHED [4/5] RUN npm install --omit=dev                                                    0.0s
 => [5/5] COPY . .                                                                             0.0s
 => exporting to image                                                                         0.1s
 => => exporting layers                                                                        0.0s
 => => exporting manifest sha256:c7dd5ef36303b2a808ee6b594946ee3c029769363a3e7eecfbc53f1c3795  0.0s
 => => exporting config sha256:b23dccadd37348cdcc5e2ca60934e15f7f7f309f477651ab90b6f3759e2e25  0.0s
 => => exporting attestation manifest sha256:217b64bdf5112df5d8d6131ebb08c0fbdf3b30db091103a9  0.0s
 => => exporting manifest list sha256:4063457524bbb10ef5c91def983dc1385e7aac61245c61fc8a4d568  0.0s
 => => naming to docker.io/library/hello-node:latest                                           0.0s
 => => unpacking to docker.io/library/hello-node:latest                                        0.0s

El contexto de compilación se redujo a 159B. .dockerignore ofrece estas ventajas:

  1. La compilación es más rápida, especialmente en proyectos con un node_modules grande o con .git.
  2. Los cambios en estos archivos no invalidan la caché de COPY . ..
  3. Evita incluir archivos confidenciales, como .env y claves privadas, en la imagen.

El node_modules de tu equipo puede contener módulos nativos compilados para Windows o macOS. Copiarlos a un contenedor Linux puede causar errores. En los proyectos Node.js, excluye siempre node_modules en .dockerignore.

Detener un contenedor

$ time docker stop hello
hello
real	0m 0.69s
user	0m 0.01s
sys	0m 0.00s

El contenedor se detuvo en 0,7 segundos. El proceso principal del contenedor es npm. Cuando recibe la señal SIGTERM enviada por docker stop, se la reenvía a node y termina correctamente. Si usas node directamente como proceso principal, debes gestionar SIGTERM en el programa; de lo contrario, docker stop esperará los 10 segundos completos. Consulta las pruebas en Referencia de instrucciones de Dockerfile.

Consejos

  • Elige una imagen base con la variante alpine o slim para reducir considerablemente el tamaño de la imagen (consulta la comparación de tamaños en Imágenes de Docker).
  • Fija la versión de la imagen base, por ejemplo, node:24-alpine; no escribas solo node.
  • Se recomienda usar el formato de matriz JSON (["npm", "start"]) con CMD, porque su comportamiento es más claro.

En la siguiente sección se explican en detalle todas las instrucciones de Dockerfile.

Resumen

  • Dockerfile empieza con FROM y cada instrucción genera una capa.
  • docker build -t nombre . crea la imagen; el . final indica el contexto de compilación.
  • Copia primero la lista de paquetes y luego instálalos. Así, la caché se puede reutilizar cuando solo cambie el código.
  • .dockerignore excluye los archivos innecesarios, como node_modules, .git y .env.

Ejercicios prácticos

Ejercicio 1: Después de modificar el texto de respuesta de server.js y volver a construir la imagen, ¿qué pasos muestran CACHED?

Respuesta

FROM, WORKDIR, COPY package*.json y RUN npm install muestran CACHED; solo se vuelven a ejecutar los pasos posteriores a COPY . ., porque solo cambió el código.

Ejercicio 2: Mueve COPY package*.json ./ y RUN npm install del Dockerfile a después de COPY . .; luego, modifica server.js otra vez y vuelve a construir la imagen. ¿Qué cambia?

Respuesta

RUN npm install también se volverá a ejecutar y la compilación será más lenta. Como cambió el contenido de COPY . ., hay que reconstruir todas las capas posteriores. Consulta la caché de compilación.

Preguntas frecuentes

¿El archivo Dockerfile tiene que llamarse necesariamente Dockerfile?

Ese es el nombre predeterminado. Para usar otro nombre, indícalo con docker build -f nombre .; consulta los parámetros en [docker build](/es/docker/docker-build/).

¿Tengo que instalar Node.js en mi ordenador?

No. Node.js está en la imagen, y tanto la compilación como la ejecución se realizan en el contenedor.

¿Hay alguna herramienta que genere un Dockerfile si no quiero escribirlo?

Docker Desktop incluye docker init. Responde unas preguntas para generar un Dockerfile y compose.yaml; puedes ver una prueba en [docker init](/es/docker/docker-init/).

Actualizado 14 de octubre de 2026