Francisco Javier Palacios PérezFco. Javier Palacios Pérez
Desarrollador de software
Introducción a los Dockerfiles: construye tus propias imágenes

Introducción a los Dockerfiles: construye tus propias imágenes

Introducción a los Dockerfiles: construye tus propias imágenes

Introducción a los Dockerfiles: construye tus propias imágenes

Tienes una aplicación funcionando en tu máquina. Un servidor web en Python, una API en Node, lo que sea. Ahora alguien te dice: “métela en Docker”. Y te das cuenta de que hasta ahora solo has usado imágenes oficiales de Docker Hub — Nginx, Ubuntu, Alpine — imágenes que alguien más construyó y tú simplemente descargaste. Pero tu aplicación no está en Docker Hub. Tu aplicación eres tú.

¿Cómo empaquetas algo propio? ¿Cómo le dices a Docker qué tiene que instalar, qué archivos copiar, qué comando ejecutar al arrancar? ¿Cómo se construye una imagen desde cero?

La respuesta es un Dockerfile.

Qué es un Dockerfile

Un Dockerfile es un archivo de texto — literalmente se llama Dockerfile, sin extensión — que contiene las instrucciones para construir una imagen Docker. Cada instrucción es un paso: parte de esta imagen base, instala estos paquetes, copia estos archivos, ejecuta este comando al arrancar.

La analogía más directa es una receta de cocina. La receta no es el plato — es el conjunto de instrucciones para producirlo de forma reproducible. El Dockerfile no es la imagen; es lo que le dice a Docker cómo construirla. Y la imagen no es el contenedor en marcha; es el resultado de seguir esa receta, listo para instanciar cuantas veces quieras.

Receta → imagen → contenedor. Dockerfile → docker build → docker run.

Lo que hace especialmente potente a un Dockerfile es que ese proceso es reproducible y versionable. El mismo Dockerfile produce la misma imagen en tu máquina, en la de tu compañero, en el servidor de CI, y en producción. Sin “en mi máquina funcionaba”. Sin “no sé qué tengo instalado”. Sin configuración manual que nadie documentó.

Las instrucciones básicas

Un Dockerfile tiene su propio lenguaje: instrucciones en mayúsculas, cada una con su propósito. ¿Cuántas hay? Bastantes. ¿Necesitas saberlas todas ahora? No. Con seis instrucciones puedes construir el 90% de las imágenes que vas a necesitar en la vida real. Vamos con esas seis:

FROM — la imagen base

Todo Dockerfile empieza con FROM. Define la imagen sobre la que construyes — la base de tu imagen.

FROM python:3.12-slim

No construyes desde cero absoluto. Partes de una imagen existente que ya tiene el sistema operativo, el runtime, las herramientas básicas. python:3.12-slim es Python 3.12 sobre Debian con los paquetes mínimos necesarios. A partir de ahí, añades lo tuyo.

WORKDIR — el directorio de trabajo

WORKDIR /app

Establece el directorio de trabajo dentro del contenedor. Todos los comandos posteriores (RUN, COPY, CMD) se ejecutan desde aquí. Si el directorio no existe, Docker lo crea. Es el equivalente de hacer mkdir /app && cd /app, pero de forma explícita y declarativa.

Sin WORKDIR, tus archivos acaban esparcidos por el sistema de archivos del contenedor y nadie sabe dónde está nada. Ponlo siempre.

COPY — copiar archivos al contenedor

COPY requirements.txt .
COPY . .

Copia archivos desde el contexto de build (tu máquina) al sistema de archivos del contenedor. El primer argumento es el origen (relativo al directorio donde está el Dockerfile), el segundo es el destino dentro del contenedor.

COPY requirements.txt . copia solo el archivo de dependencias. COPY . . copia todo el resto. El orden no es arbitrario — ahí está el corazón del sistema de caché de Docker, que veremos en la próxima lección.

RUN — ejecutar comandos durante el build

RUN pip install --no-cache-dir -r requirements.txt

Ejecuta un comando durante la construcción de la imagen y guarda el resultado como una nueva capa. Aquí instalas dependencias, compilas código, creas directorios, lo que necesites. El resultado queda frozen en la imagen — no se ejecuta cada vez que arranca un contenedor, solo cuando construyes la imagen.

EXPOSE — documentar qué puertos usa la app

EXPOSE 5000

Declara en qué puerto escucha la aplicación dentro del contenedor. Ojo: esto es documentación, no mapeo de puertos. No publica el puerto al host — para eso sigue siendo necesario -p al hacer docker run. Pero es información útil para quien use la imagen, y algunas herramientas la leen.

CMD — el comando que arranca la app

CMD ["python", "app.py"]

Define el proceso principal del contenedor — lo que se ejecuta cuando haces docker run. Solo puede haber un CMD por Dockerfile (si pones varios, solo cuenta el último). Usa la forma de array (["python", "app.py"]), no la forma de string — evita problemas con señales del sistema operativo.

Tu primer Dockerfile completo

Una aplicación Flask mínima — el tipo de cosa que tienes corriendo en local y que ahora vamos a meter en una imagen:

# app.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello from Docker!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
# requirements.txt
flask==3.0.3
# Dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

Construir la imagen con docker build

docker build -t mi-flask-app .
  • -t mi-flask-app — el nombre (tag) de la imagen resultante
  • . — el contexto de build: el directorio desde el que Docker copia archivos. Casi siempre es el directorio actual
[+] Building 23.4s (9/9) FINISHED
 => [internal] load build definition from Dockerfile
 => [internal] load .dockerignore
 => [internal] load metadata for docker.io/library/python:3.12-slim
 => [1/4] FROM python:3.12-slim
 => [2/4] WORKDIR /app
 => [3/4] COPY requirements.txt .
 => [4/4] RUN pip install --no-cache-dir -r requirements.txt
 => [5/4] COPY . .
 => exporting to image
 => naming to docker.io/library/mi-flask-app

Cada línea del output es una capa. Docker construye la imagen paso a paso, y ese output desfilando — que al principio parece ruido — en realidad es el sistema de capas trabajando. En la próxima lección verás por qué ese orden importa muchísimo para el rendimiento.

Cuando termina, la imagen está disponible localmente:

docker images
REPOSITORY      TAG       IMAGE ID       CREATED          SIZE
mi-flask-app    latest    a1b2c3d4e5f6   12 seconds ago   148MB

Ejecutar el contenedor

docker run -d -p 5000:5000 --name mi-app mi-flask-app

http://localhost:5000 devuelve Hello from Docker!. Tu aplicación, en un contenedor, construida desde tu propio Dockerfile.

El contexto de build y por qué .dockerignore es obligatorio

Cuando ejecutas docker build ., Docker empaqueta todo el contenido del directorio . y lo envía al daemon para que pueda acceder a esos archivos durante el build. Eso se llama el contexto de build.

El problema es “todo el contenido”. Si tienes node_modules con sus 200.000 archivos que todos fingimos no ver, o un directorio .git con el historial completo del proyecto, o archivos de entorno con credenciales — todo eso se envía. El build se ralentiza, la imagen crece, y si alguien mete un .env con contraseñas en el contexto, esas contraseñas acaban horneadas dentro de la imagen para siempre. Aquí es donde las cosas se ponen innecesariamente peligrosas.

La solución es .dockerignore, que funciona exactamente igual que .gitignore:

# .dockerignore
.git
.env
node_modules
__pycache__
*.pyc
.DS_Store        # (porque macOS, ya sabes)

Ponlo siempre. No es opcional.


Con esto ya puedes empaquetar cualquier aplicación en una imagen Docker. Dockerfile → docker build → imagen → docker run → contenedor. El ciclo completo bajo tu control.

Pero ahora mismo tu imagen tarda lo mismo en construirse tanto si cambias una línea de tu app como si instalas todas las dependencias desde cero. En la próxima lección vemos cómo funciona el sistema de caché por capas de Docker — y cómo ordenar correctamente las instrucciones del Dockerfile para que los builds sean diez veces más rápidos.

¡Nunca dejes de programar!


💡 Reto: Crea una aplicación Flask mínima con el Dockerfile de esta lección. Construye la imagen, lanza el contenedor y verifica que responde en http://localhost:5000. Luego modifica solo el mensaje de respuesta de la app, reconstruye y comprueba cuántas capas se reconstruyen desde cero. ¿Ves el patrón? Lo explicamos en detalle en la próxima lección.