Cambiar tema

Tutorial introductorio de Dockerfile: construye tu primera imagen Docker desde cero (con ejemplo)

Easton editorial illustration: image-layer stack

Los errores que se desplazan en la terminal: «COPY ../config.json: no such file or directory». La octava vez que falla el build esta noche. En local funciona, pero al empaquetarlo en una imagen Docker aparecen errores por todas partes. Revisas Stack Overflow, sigues la respuesta más votada y, al rato, la imagen pasa de 200 MB a 2 GB.

Miras el Dockerfile del proyecto: FROM, RUN, COPY, CMD por todas partes. Cada palabra la entiendes por separado; juntas, no. Copias tutoriales de internet y nueve de cada diez no arrancan. Los mensajes de error son siempre vagos: ¿ruta incorrecta o instrucción mal usada?

180 veces
Diferencia de tamaño
Alpine vs imagen completa
10 veces
Reducción de volumen
Fusionar instrucciones RUN
30 veces
Aceleración del build
5 min → 10 s
Source: Datos medidos

Un Dockerfile no es magia. Cuando entiendes qué hace cada instrucción y los fallos habituales, todo se reduce a unos pocos puntos. Este artículo explica, de forma directa, cómo construir tu primera imagen Docker desde cero. Cada instrucción lleva código real y te señalamos los 3 errores que más suelen cometer los principiantes. Al terminar, podrás escribir un Dockerfile funcional para un proyecto Node.js o Python.

¿Qué es un Dockerfile?

En pocas palabras, un Dockerfile es un archivo de texto con todos los pasos para construir una imagen Docker. Piénsalo como el plano de una reforma: primero el suelo, luego las paredes, al final la iluminación. El motor de Docker sigue ese plano paso a paso, «reforma» tu aplicación y la empaqueta en una imagen.

Esa imagen es una «foto» del entorno lista para ejecutarse. Si tu app Node.js necesita Node 18, ciertos paquetes npm y tu código, el Dockerfile mete todo dentro. Quien reciba la imagen solo tiene que hacer docker run, sin pelearse con el entorno.

En la práctica, un Dockerfile hace tres cosas:

  1. Elegir un entorno base (por ejemplo Node.js 18)
  2. Meter tu código y dependencias
  3. Decir qué comando ejecutar al arrancar el contenedor

Cuando está listo, un docker build genera la imagen. Suena simple, y lo es en líneas generales; el diablo está en los detalles. Vamos con las instrucciones clave.

Las 6 instrucciones clave

Flujo completo para construir una imagen Docker desde cero

Paso a paso para escribir un Dockerfile, desde la imagen base hasta construir y ejecutar tu primera imagen

Estimated time: PT20M

  1. 1

    Step 1: Paso 1: Elegir imagen base (FROM)

    FROM debe ser la primera instrucción; elige la base adecuada:
  2. 2

    Step 2: Paso 2: Directorio de trabajo (WORKDIR)

    Usa WORKDIR /app
  3. 3

    Step 3: Paso 3: Copiar dependencias e instalar (COPY+RUN)

    Copia primero package*.json y luego npm install
  4. 4

    Step 4: Paso 4: Copiar código de la app (COPY)

    Copia el código fuente al contenedor
  5. 5

    Step 5: Paso 5: Exponer puerto (EXPOSE)

    EXPOSE 3000 declara el puerto del contenedor
  6. 6

    Step 6: Paso 6: Comando de arranque (CMD)

    CMD [“npm”, “start”] define el comando por defecto al arrancar
  7. 7

    Step 7: Paso 7: Construir y ejecutar

    Construir la imagen:

FROM - Elegir la imagen base

FROM debe ser la primera instrucción del Dockerfile (salvo comentarios y ARG). Define los «cimientos» de tu imagen.

Como al construir una casa, primero eliges la base. ¿App en Node.js? node:18-alpine. ¿Proyecto Python? python:3.11-slim encaja bien. ¿Nginx como proxy inverso? Directamente nginx:alpine.

# Elegir Node.js 18 en Alpine Linux como imagen base
FROM node:18-alpine

Sobre alpine y slim: alpine es una imagen ultraligera basada en Alpine Linux, unos 5 MB, ideal para producción. La imagen node completa ronda los 900 MB, 180 veces más. El matiz: usa musl libc en lugar de glibc; algunas dependencias nativas pueden fallar. Si ves errores de compilación raros, prueba node:18-slim.

Error típico de principiante: elegir cualquier imagen node y que la versión no coincida con el proyecto; al instalar dependencias, errores en cadena. Regla: la versión de Node en package.json es la que pones en FROM.

RUN - Ejecutar comandos al construir

RUN ejecuta comandos durante la construcción: instalar software, crear directorios, editar configuración. Cada RUN crea una nueva capa de imagen.

Un mal ejemplo:

# ❌ Mal: crea 3 capas
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get clean

Cada RUN añade una capa, como una cebolla. Aunque borres archivos después, los datos de capas anteriores siguen ahí y el tamaño no baja. Así acabé yo con 7 RUN y una imagen de 2 GB; media hora subiéndola al servidor.

La forma correcta es encadenar con &&:

# ✅ Recomendado: una sola capa
RUN apt-get update && \
    apt-get install -y python3 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

La barra invertida \ es salto de línea para que se lea mejor. El rm final limpia la caché de paquetes y ahorra decenas de MB.

Otro fallo: nunca escribas solo RUN apt-get update. Docker cachea cada capa; si update va solo, install puede usar caché vieja y no instalar versiones nuevas. Siempre junta update e install.

COPY vs ADD - Copiar archivos

Ambas copian archivos a la imagen, pero COPY es directo; ADD hace más cosas y puede sorprender. La recomendación oficial: usa COPY siempre que puedas.

COPY básico:

# Copiar un archivo
COPY package.json /app/

# Copiar un directorio completo
COPY ./src /app/src

# Copiar todo el directorio actual a /app del contenedor
COPY . /app

Parece simple, pero hay un error mortal: las rutas son relativas al contexto de build, no al Dockerfile.

¿Qué es el contexto de build? El directorio del punto (.) al final de docker build. Si ejecutas docker build . en la raíz del proyecto, el contexto es esa raíz; COPY solo ve ese directorio y sus hijos.

De ahí el error del principio:

# ❌ Mal: fuera del contexto de build
COPY ../config.json /app/
COPY /opt/myfile.txt /app/

El primero intenta un directorio superior; el segundo, una ruta absoluta. Ambos fallan. Docker lo limita por seguridad y builds reproducibles.

Soluciones:

  • Mueve config.json al directorio del proyecto
  • O construye desde arriba: docker build -f myproject/Dockerfile .

Sobre ADD: además de copiar, descomprime tar y puede descargar URLs:

# ADD descomprime automáticamente
ADD myarchive.tar.gz /app/

# ADD puede descargar URL (no recomendado)
ADD https://example.com/file.txt /app/

Suena útil, pero el comportamiento no es obvio. Mejor RUN tar -xzf para descomprimir y RUN curl para descargar, explícito.

WORKDIR - Directorio de trabajo

WORKDIR es el cd de Linux: fija el directorio para las instrucciones siguientes. Si no existe, Docker lo crea.

WORKDIR /app
COPY . .  # Ahora copia a /app
RUN npm install  # Se ejecuta en /app

Usa rutas absolutas; las relativas se apilan sobre el WORKDIR anterior y confunden.

Con WORKDIR evitas cd /app && en cada RUN; el Dockerfile queda más limpio.

CMD vs ENTRYPOINT - Comando de arranque del contenedor

Son las que más se confunden. Regla simple: CMD se puede sobrescribir; ENTRYPOINT no.

CMD define el comando por defecto al arrancar:

CMD ["node", "server.js"]

Con docker run my-app se ejecuta node server.js. Con docker run my-app npm test, CMD se sustituye y corre npm test.

ENTRYPOINT fija el proceso principal; no se sobrescribe:

ENTRYPOINT ["node"]
CMD ["server.js"]

docker run my-appnode server.js. docker run my-app script.jsnode script.js. ENTRYPOINT queda fijo; CMD o los argumentos de docker run se añaden detrás.

¿Cuándo usar cada uno?

  • Solo CMD: servicios con distintos arranques (producción npm start, pruebas npm test)
  • ENTRYPOINT + CMD: herramientas; comando fijo, parámetros variables (Python: python + nombre del script)
  • Solo ENTRYPOINT: contenedor con una sola función muy fija

Comparación:

# Escenario 1: app web (CMD)
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["npm", "start"]
# docker run my-app → npm start
# docker run my-app npm test → npm test (CMD sobrescrito)

# Escenario 2: herramienta Python (ENTRYPOINT + CMD)
FROM python:3.11-slim
ENTRYPOINT ["python"]
CMD ["main.py"]
# docker run my-tool → python main.py
# docker run my-tool script.py → python script.py

Al principio también me liaba; luego lo resumí: ENTRYPOINT es «qué hacer», CMD es «cómo hacerlo».

ENV - Variables de entorno

ENV define variables que viven en el contenedor en ejecución. Puedes usarlas en RUN, CMD, etc.

ENV NODE_ENV=production
ENV PORT=3000

# En RUN
RUN echo "Environment: $NODE_ENV"

# La app también las lee en runtime
CMD ["node", "server.js"]

Usos habituales:

  • NODE_ENV=production para Node en producción
  • Ampliar PATH con rutas propias
  • Parámetros de la app (puerto, base de datos, etc.)

Las variables ENV quedan en la imagen final; no pongas secretos ahí. Pásalos con docker run -e o Docker Secrets.

Práctica: construir tu primera imagen

Tanta teoría sin manos no basta. Con una app Node.js simple, construimos la primera imagen paso a paso.

Paso 1: Preparar el proyecto

Crea una app Node.js mínima:

mkdir my-node-app
cd my-node-app

Crea package.json:

{
  "name": "my-node-app",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

Crea server.js:

const express = require('express');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello from Docker!');
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Paso 2: Escribir el Dockerfile

En la raíz del proyecto, crea Dockerfile (sin extensión):

# 1. Imagen base
FROM node:18-alpine

# 2. Directorio de trabajo
WORKDIR /app

# 3. Copiar dependencias (aprovechar caché)
COPY package*.json ./

# 4. Instalar dependencias
RUN npm install --production

# 5. Copiar código
COPY . .

# 6. Exponer puerto
EXPOSE 3000

# 7. Arrancar la app
CMD ["npm", "start"]

¿Por qué copiar package.json y el código por separado? Por la caché de Docker.

Docker construye capa a capa en orden. Si una capa cambia, las siguientes se reconstruyen. package.json cambia poco; el código, mucho. Si copias todo y luego instalas, cada cambio de código fuerza reinstalar dependencias.

Con este orden, si package.json no cambia, Docker reutiliza la capa de dependencias y solo reconstruye la copia de código. Mucho más rápido.

Paso 3: Construir la imagen

En la raíz del proyecto:

docker build -t my-node-app:1.0 .

Parámetros:

  • -t my-node-app:1.0: etiqueta de la imagen, formato nombre:versión
  • .: contexto de build, directorio actual

Verás salida por cada instrucción del Dockerfile. Si todo va bien, al final: Successfully built xxx.

Paso 4: Ejecutar el contenedor

docker run -p 3000:3000 my-node-app:1.0

Parámetros:

  • -p 3000:3000: mapeo de puertos, puerto_host:puerto_contenedor
  • my-node-app:1.0: imagen a ejecutar

Verás «Server running on port 3000» en la terminal.

Paso 5: Comprobar

Abre el navegador en http://localhost:3000. Si ves «Hello from Docker!», listo.

Pulsa Ctrl+C para detener el contenedor.

Resumen del flujo

El proceso completo:

  1. Escribir código (package.json + server.js)
  2. Escribir Dockerfile (cómo empaquetar)
  3. Construir imagen (docker build)
  4. Ejecutar contenedor (docker run)

No es tan difícil. Lo importante es entender cada instrucción, el contexto de build y la caché.

Guía de errores para principiantes

Después de lo correcto, los fallos que más tiempo quitan. Los he pisado todos; esto te ahorra horas.

Error 1: Rutas incorrectas en el contexto de build

Síntoma: COPY falla con «no such file or directory» aunque el archivo «esté ahí».

Causa: las rutas de COPY son relativas al contexto de build, no al Dockerfile.

# ❌ Mal: directorio superior
COPY ../config.json /app/

# ❌ Mal: ruta absoluta
COPY /opt/myfile.txt /app/

Solución:

  1. Mueve el archivo al directorio del proyecto
  2. O ajusta el build: docker build -f subdir/Dockerfile . (-f indica el Dockerfile; el punto sigue siendo el contexto superior)

Trampa oculta: si ejecutas docker build . en la raíz, Docker envía todo el directorio al daemon, incluidos node_modules y .git. Yo una vez mandé varios GB y esperé 10 minutos antes de que empezara el build.

Solución: archivo .dockerignore:

node_modules
.git
.env
*.log

Error 2: Demasiadas capas e imagen hinchada

Síntoma: imagen enorme; el código pesa pocos MB y la imagen varios GB.

Causa: cada RUN/COPY/ADD crea una capa; borrar después no quita datos de capas anteriores.

# ❌ 7 capas; cada una conserva datos
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN curl -o tool.sh https://example.com/tool.sh
RUN chmod +x tool.sh
RUN ./tool.sh
RUN rm tool.sh  # ¡No sirve! tool.sh sigue en capas anteriores

Solución: fusionar RUN; instalar, usar y limpiar en la misma capa:

# ✅ Una capa; la limpieza sí cuenta
RUN apt-get update && \
    apt-get install -y curl git && \
    curl -o tool.sh https://example.com/tool.sh && \
    chmod +x tool.sh && \
    ./tool.sh && \
    rm tool.sh && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Yo tenía 7 RUN separados y 2 GB; fusionando, 200 MB, 10 veces menos.

10 veces
Reducción de volumen
Source: Fusionar RUN: 2 GB → 200 MB

Error 3: Caché de dependencias rota

Síntoma: cada build reinstala dependencias; muy lento.

Causa: orden de COPY incorrecto; código y dependencias juntos; cualquier cambio de código dispara npm install.

# ❌ Mal orden: cambiar código = reinstalar
COPY . .
RUN npm install

La caché es secuencial. Si cambia el código (hasta un comentario), cambia la capa COPY y npm install se vuelve a ejecutar.

Solución: primero dependencias, luego código:

# ✅ Solo reinstala si cambia package.json
COPY package*.json ./
RUN npm install
COPY . .

Cambiar solo el código no reinstala dependencias; el build pasa de 5 minutos a 10 segundos.

Nota extra: EXPOSE no es obligatorio

Muchos tutoriales ponen EXPOSE 3000 y parece que sin eso no hay puerto. EXPOSE solo documenta; la imagen funciona igual.

Lo que mapea puertos es docker run -p:

# Funciona aunque el Dockerfile no tenga EXPOSE
docker run -p 3000:3000 my-app

Aun así, conviene escribir EXPOSE para quien use la imagen después.

Conclusión

Para empezar con Dockerfile, tres ideas:

  1. Instrucciones clave: FROM la base, RUN instala, COPY copia, CMD arranca; WORKDIR y ENV ayudan
  2. Contexto de build: COPY solo ve el directorio del punto (.) de docker build; no rutas superiores ni absolutas
  3. Caché: primero lo que cambia poco (dependencias), después el código; fusiona RUN para menos capas

Prueba con un proyecto pequeño: un Dockerfile mínimo que arranque. Cuando fluya, pasa a multi-stage build y optimización de tamaño.

Docker no es imposible; hace falta practicar. Mi primer Dockerfile falló toda una noche; cuando encajó, vi que era más simple de lo que parecía. Tú también puedes.

Qué aprender después:

  • Docker Compose (varios contenedores)
  • Multi-stage build (imágenes más pequeñas)
  • Redes y volúmenes Docker (comunicación y persistencia)

¡Ánimo con tu primera imagen Docker! Si tienes dudas, deja un comentario.

FAQ

¿Cuáles son las instrucciones clave de un Dockerfile?
6 instrucciones clave:
1) FROM elige la imagen base (debe ser la primera)
2) RUN ejecuta comandos durante la construcción (usa && para fusionar y reducir capas)
3) COPY copia archivos (las rutas son relativas al contexto de build; no puedes usar ../ ni rutas absolutas)
4) WORKDIR define el directorio de trabajo (se recomienda ruta absoluta)
5) CMD define el comando de arranque del contenedor (puede sobrescribirse)
6) ENV define variables de entorno

También están ENTRYPOINT (comando principal fijo) y EXPOSE (declara puertos; solo documentación).
¿Por qué COPY ../config.json da error?
Las rutas de COPY son relativas al contexto de build, no al Dockerfile.

El contexto de build es el directorio que indicas con el punto (.) al final del comando docker build. COPY solo puede acceder a ese directorio y sus subdirectorios; no a directorios superiores ni a rutas absolutas.

Soluciones:
• Mueve el archivo dentro del directorio del proyecto
• O ajusta el comando de build: docker build -f subdir/Dockerfile .

Crear un .dockerignore para excluir node_modules, .git y otras carpetas grandes acelera la construcción.
¿Cómo reducir el tamaño de una imagen Docker?
Tres métodos:

1) Usar imágenes Alpine:
• Unos 5 MB frente a ~900 MB de la imagen completa, una diferencia de 180 veces

2) Fusionar instrucciones RUN:
• Encadena con && para instalar, usar y limpiar en la misma capa
• Puede bajar la imagen de 2 GB a 200 MB, una reducción de 10 veces

3) Crear .dockerignore para excluir archivos innecesarios

Escribe siempre apt-get update e install juntos para no usar caché antigua.
¿Cómo aprovechar la caché de Docker para acelerar el build?
La clave: pon primero lo que cambia poco y al final lo que cambia mucho.

Orden correcto:
• Copia primero package*.json e instala dependencias
• Luego copia el código fuente
• Si package.json no cambia, la capa de dependencias no se reconstruye
• El build pasa de 5 minutos a 10 segundos (30 veces más rápido)

Orden incorrecto:
• Copiar todo y luego instalar dependencias
• Cada cambio de código obliga a reinstalar dependencias; muy lento
¿Cuál es la diferencia entre CMD y ENTRYPOINT?
CMD puede sobrescribirse con los argumentos de docker run; ENTRYPOINT no.

Cuándo usar cada uno:
1) Solo CMD: servicios de aplicación con distintos modos de arranque (producción npm start, pruebas npm test)
2) ENTRYPOINT+CMD: imágenes tipo herramienta, comando fijo con parámetros variables (p. ej. scripts Python)
3) Solo ENTRYPOINT: escenarios muy fijos

Recuerda: ENTRYPOINT es «qué hacer», CMD es «cómo hacerlo».
¿Es obligatoria la instrucción EXPOSE?
No es obligatoria. EXPOSE solo documenta qué puerto usa la imagen; funciona igual sin escribirla.

El mapeo real lo controla docker run -p. Aunque el Dockerfile no tenga EXPOSE, docker run -p 3000:3000 my-app sigue funcionando.

Aun así, conviene añadir EXPOSE para que otros entiendan qué puerto usa la imagen.
¿Qué diferencia hay entre imágenes Alpine y slim?
Alpine se basa en Alpine Linux:
• Unos 5 MB; ideal para producción
• Usa musl libc en lugar de glibc; algunas dependencias nativas pueden fallar
• La imagen completa ronda los 900 MB, 180 veces más

Si ves errores de compilación raros, prueba una versión slim (p. ej. node:18-slim).

Regla práctica: prioriza Alpine; si hay problemas de compatibilidad, cambia a slim.

12 min de lectura · Publicado el: 17 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog