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:
| Paso | Instrucción Dockerfile correspondiente | Descripción |
|---|---|---|
load build definition | — | Lee el Dockerfile |
load metadata for …node:24-alpine | FROM | Consulta la versión más reciente de la imagen base |
[1/5] FROM … | FROM node:24-alpine | Descarga 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 /app | WORKDIR | Crea /app y cambia a esa carpeta |
[3/5] COPY package*.json ./ | COPY | Copia el manifiesto de paquetes |
[4/5] RUN npm install --omit=dev | RUN | Instala los paquetes en la imagen |
[5/5] COPY . . | COPY | Copia 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

- La imagen ocupa unos 245 MB; la mayor parte corresponde al propio Node.js (node:24-alpine).
- En
docker logs,> node server.jses un mensaje que imprimenpm start. A continuación apareceListening 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 ejecutarnpm 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:
- La compilación es más rápida, especialmente en proyectos con un
node_modulesgrande o con.git. - Los cambios en estos archivos no invalidan la caché de
COPY . .. - Evita incluir archivos confidenciales, como
.envy claves privadas, en la imagen.
El
node_modulesde 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 siemprenode_modulesen.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
alpineoslimpara 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 solonode. - Se recomienda usar el formato de matriz JSON (
["npm", "start"]) conCMD, 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
FROMy 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.
.dockerignoreexcluye los archivos innecesarios, comonode_modules,.gity.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