El Dockerfile es el corazón de la creación de imágenes en Docker: un fichero de texto donde describimos, paso a paso, cómo se construye una imagen. Dominar sus instrucciones es imprescindible para crear imágenes correctas, eficientes y mantenibles. En esta lección estudiaremos qué es un Dockerfile y cómo funciona, repasaremos en una tabla las instrucciones esenciales (FROM, RUN, COPY, ADD, WORKDIR, CMD, ENTRYPOINT, EXPOSE, ENV), aclararemos la confusión clásica entre CMD y ENTRYPOINT, y analizaremos un Dockerfile completo comentado línea a línea.

¿Qué es un Dockerfile?

Un Dockerfile es un fichero de texto plano (normalmente llamado exactamente Dockerfile, sin extensión) que contiene una secuencia de instrucciones. Cada instrucción describe un paso de la construcción, y Docker las ejecuta en orden, de arriba abajo.

Características fundamentales:

  • Cada instrucción suele escribirse en MAYÚSCULAS por convención (FROM, RUN, etc.), aunque Docker no distingue mayúsculas de minúsculas en el nombre de la instrucción.
  • La mayoría de las instrucciones generan una capa en la imagen resultante. Las capas se cachean, lo que acelera reconstrucciones posteriores.
  • Las líneas que empiezan por # son comentarios y se ignoran.
  • La primera instrucción significativa casi siempre es FROM, que define la imagen base.

Instrucciones Esenciales del Dockerfile

La siguiente tabla resume las instrucciones que usarás en la inmensa mayoría de los Dockerfiles:

Instrucción Para qué sirve Ejemplo
FROM Define la imagen base de la que partimos FROM node:18-alpine
WORKDIR Establece el directorio de trabajo dentro de la imagen WORKDIR /app
COPY Copia ficheros del contexto a la imagen COPY . .
ADD Como COPY, pero también descomprime y descarga URLs ADD app.tar.gz /app
RUN Ejecuta un comando durante la construcción RUN npm ci
ENV Define variables de entorno en la imagen ENV NODE_ENV=production
EXPOSE Documenta el puerto en que escucha el contenedor EXPOSE 3000
CMD Comando por defecto al arrancar el contenedor CMD ["node", "server.js"]
ENTRYPOINT Comando fijo que siempre se ejecuta al arrancar ENTRYPOINT ["node"]

FROM

FROM debe ser la primera instrucción (salvo argumentos ARG previos). Establece la base sobre la que se construye todo lo demás.

FROM python:3.12-slim
  • python:3.12-slim: una imagen oficial de Python 3.12 en su variante reducida (slim).

WORKDIR

Define el directorio de trabajo para las instrucciones posteriores (RUN, COPY, CMD, etc.). Si no existe, lo crea.

WORKDIR /app
  • A partir de aquí, un COPY . . copia al directorio /app, y los comandos se ejecutan desde ahí.
  • Es preferible a usar RUN cd /app, porque cd no persiste entre instrucciones; WORKDIR sí.

COPY y ADD

Ambas copian ficheros del contexto de construcción a la imagen, pero tienen diferencias importantes:

COPY requirements.txt ./
ADD https://example.com/data.tar.gz /datos/
Aspecto COPY ADD
Copiar ficheros locales
Descomprimir .tar automáticamente No
Descargar desde una URL No
Recomendación general Preferida por su transparencia Solo cuando necesitas sus extras
  • Usa COPY por defecto: es más predecible. Reserva ADD solo para los casos en que necesites descomprimir un .tar local o descargar una URL.

RUN

Ejecuta un comando durante la construcción de la imagen, generando una nueva capa con el resultado.

RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
  • Se ejecuta al construir, no al arrancar el contenedor.
  • Encadenar comandos con && y limpiar en la misma instrucción reduce el número de capas y el tamaño final.

ENV

Define variables de entorno que estarán disponibles tanto durante la construcción como en el contenedor en ejecución.

ENV NODE_ENV=production
ENV APP_PORT=3000
  • Estas variables las pueden leer la aplicación y otras instrucciones del Dockerfile.

EXPOSE

Documenta qué puerto usa el contenedor. No publica el puerto por sí mismo: solo es informativo.

EXPOSE 3000
  • Para que el puerto sea accesible desde el host, sigues necesitando -p 8080:3000 al ejecutar docker run.

CMD vs ENTRYPOINT: La Confusión Clásica

Esta es una de las dudas más frecuentes para quien empieza. Ambas definen qué se ejecuta al arrancar el contenedor, pero se comportan de forma distinta.

Aspecto CMD ENTRYPOINT
Propósito Comando por defecto, fácil de sustituir Comando fijo del contenedor
¿Se sobrescribe con docker run imagen otracosa? Sí, se reemplaza por completo No; los argumentos se le añaden
Uso típico Aplicación cuyo comando puede variar Un ejecutable concreto que siempre se usa

Veamos la diferencia con ejemplos.

Con solo CMD:

FROM ubuntu
CMD ["echo", "Hola"]
  • docker run miimg imprime Hola.
  • docker run miimg echo Adios imprime Adios: el argumento sustituye completamente al CMD.

Con solo ENTRYPOINT:

FROM ubuntu
ENTRYPOINT ["echo"]
  • docker run miimg Hola imprime Hola: el argumento se añade al ENTRYPOINT.
  • No puedes cambiar fácilmente el ejecutable; siempre será echo.

La combinación recomendada usa ENTRYPOINT para fijar el ejecutable y CMD para los argumentos por defecto:

FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["Hola por defecto"]
  • docker run miimg imprime Hola por defecto (el CMD por defecto).
  • docker run miimg Adios imprime Adios (el argumento sustituye al CMD, pero el ENTRYPOINT echo se mantiene).

Nota sobre formato: siempre que puedas, usa la forma exec (con corchetes y comillas, como ["echo", "Hola"]) en lugar de la forma shell (CMD echo Hola). La forma exec no envuelve el comando en una shell, lo que gestiona mejor las señales del sistema (por ejemplo, para detener el contenedor correctamente).

Ejemplo Completo: Dockerfile Comentado Línea a Línea

Veamos un Dockerfile realista para una aplicación Python con Flask, explicado instrucción por instrucción.

# 1. Imagen base oficial de Python, variante ligera
FROM python:3.12-slim

# 2. Variables de entorno de la aplicación
ENV PYTHONUNBUFFERED=1
ENV APP_PORT=5000

# 3. Directorio de trabajo dentro de la imagen
WORKDIR /app

# 4. Copiamos primero las dependencias para aprovechar la caché
COPY requirements.txt ./

# 5. Instalamos dependencias sin guardar la caché de pip
RUN pip install --no-cache-dir -r requirements.txt

# 6. Copiamos el resto del código de la aplicación
COPY . .

# 7. Documentamos el puerto en que escucha la app
EXPOSE 5000

# 8. Comando que arranca la aplicación
CMD ["python", "app.py"]

Explicación detallada:

  • Línea 2 (FROM): partimos de Python 3.12 en su versión slim, que reduce el tamaño respecto a la imagen completa.
  • Líneas 5-6 (ENV): PYTHONUNBUFFERED=1 hace que los print aparezcan inmediatamente en los logs; APP_PORT queda disponible para la aplicación.
  • Línea 9 (WORKDIR): fija /app como directorio de trabajo; se crea si no existe.
  • Línea 12 (COPY requirements.txt): copiamos solo el fichero de dependencias antes que el código, para que la costosa instalación se cachee y no se repita en cada cambio de código.
  • Línea 15 (RUN pip install): instala las dependencias. --no-cache-dir evita que pip deje su caché dentro de la imagen, reduciendo el tamaño.
  • Línea 18 (COPY . .): copia el resto del proyecto. Al ir después de la instalación, cambiar el código no invalida la capa de dependencias.
  • Línea 21 (EXPOSE 5000): documenta el puerto; recuerda que para acceder desde fuera hay que mapearlo con -p al ejecutar.
  • Línea 24 (CMD): define el comando por defecto. En forma exec, gestiona correctamente las señales de parada.

Para construir y ejecutar esta imagen:

docker build -t flaskapp:1.0 .
docker run -d -p 8080:5000 flaskapp:1.0
  • La aplicación quedará accesible en http://localhost:8080.

Errores Comunes y Consejos

  • Usar ADD cuando basta COPY: ADD tiene comportamientos implícitos (descompresión, descargas) que pueden sorprender. Usa COPY salvo que necesites lo extra.
  • Pensar que EXPOSE publica el puerto: no lo hace; solo documenta. Necesitas -p en docker run.
  • Confundir CMD y ENTRYPOINT: CMD se reemplaza entero al pasar argumentos; ENTRYPOINT los recibe como añadidos.
  • Usar la forma shell por costumbre: prefiere la forma exec ["ejecutable", "arg"] para una correcta gestión de señales.
  • Copiar todo el código antes de instalar dependencias: rompe la caché en cada cambio. Copia primero los manifiestos.
  • Usar cd dentro de RUN: no persiste entre instrucciones. Usa WORKDIR.
  • Consejo: comenta tus Dockerfiles con # para explicar decisiones no obvias; facilita el mantenimiento por parte de otros.

Ejercicios

Ejercicio 1

Escribe un Dockerfile para una aplicación Node.js que use node:18-alpine, fije el directorio de trabajo en /app, instale dependencias aprovechando la caché, copie el código, exponga el puerto 3000 y arranque con node server.js.

Ejercicio 2

Dado este Dockerfile, indica qué imprime cada comando:

FROM ubuntu
ENTRYPOINT ["echo", "Mensaje:"]
CMD ["por defecto"]
  • docker run miimg
  • docker run miimg personalizado

Ejercicio 3

Explica cuándo usarías ADD en lugar de COPY y por qué COPY es la opción preferida por defecto.

Soluciones

Solución 1

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Se copian primero los manifiestos package*.json y se ejecuta npm ci antes de copiar el resto del código, de modo que la instalación de dependencias se cachee y solo se repita cuando cambien las dependencias.

Solución 2

  • docker run miimg imprime Mensaje: por defecto. El ENTRYPOINT fija echo "Mensaje:" y el CMD aporta el argumento por defecto por defecto.
  • docker run miimg personalizado imprime Mensaje: personalizado. El argumento personalizado sustituye al CMD, pero el ENTRYPOINT se mantiene.

Solución 3

Usarías ADD cuando necesites descomprimir automáticamente un fichero .tar local o descargar un recurso desde una URL, ya que COPY no hace ninguna de esas cosas. COPY es la opción preferida por defecto porque su comportamiento es explícito y predecible: solo copia ficheros, sin acciones implícitas que puedan dar resultados inesperados.

Conclusión

En esta lección hemos profundizado en el Dockerfile: qué es y cómo se ejecuta, las instrucciones esenciales (FROM, WORKDIR, COPY, ADD, RUN, ENV, EXPOSE, CMD, ENTRYPOINT), la diferencia clave entre CMD y ENTRYPOINT, y un ejemplo completo comentado línea a línea. Con estos fundamentos ya puedes escribir Dockerfiles claros y eficientes para tus propias aplicaciones.

En la siguiente lección, Gestionando Imágenes Docker, aprenderemos a administrar las imágenes una vez construidas: listarlas, eliminarlas, inspeccionarlas, limpiar las que ya no usas con image prune, exportarlas e importarlas con save/load, y controlar el espacio que ocupan en disco.

© Copyright 2026. Todos los derechos reservados