Saltar al contenido

lección 4

Imágenes y Dockerfiles: la receta de tu entorno

Qué es una imagen, cómo se construye capa a capa, y cómo escribir tu propio Dockerfile con FROM, RUN, COPY y CMD.

⏱ 20 min

Hasta ahora has usado imágenes que crearon otros: python, node, postgres. Alguien escribió una receta para cada una de ellas, la publicó en Docker Hub, y tú la descargaste con docker run. Pero el verdadero poder de Docker llega cuando TÚ escribes tus propias recetas. En esta lección vas a aprender a crear imágenes personalizadas con un Dockerfile — el archivo de texto que describe tu entorno exacto, paso a paso, ingrediente a ingrediente.

Volvamos a la analogía del restaurante. Hasta ahora has sido un cliente que pide del menú (imágenes pre-hechas). A partir de hoy eres el chef que ESCRIBE el menú. Tu Dockerfile es la receta maestra que cualquier cocinero puede seguir para obtener exactamente el mismo plato. No importa si cocina en Madrid, en Buenos Aires o en un servidor de AWS — la receta produce el mismo resultado.

## ¿Qué es una imagen Docker, realmente?

Una imagen Docker es un paquete inmutable (que no cambia) que contiene TODO lo necesario para ejecutar una aplicación: sistema operativo base, librerías del sistema, tu código, dependencias de tu código, y la configuración. Internamente, una imagen está compuesta de CAPAS apiladas — como un pastel de capas donde cada capa añade algo encima de la anterior.

Una imagen Docker se construye por capas. Las capas se cachean — si no cambian, no se reconstruyen.

La belleza del sistema de capas es el CACHING. Si reconstruyes una imagen y las primeras 3 capas no han cambiado, Docker las reutiliza del cache y solo reconstruye a partir de la capa que sí cambió. Esto hace que las builds sean increíblemente rápidas después de la primera vez.

## Tu primer Dockerfile

Un Dockerfile es simplemente un archivo de texto (sin extensión) llamado exactamente "Dockerfile" que contiene instrucciones secuenciales. Cada instrucción crea una capa nueva en tu imagen. Vamos a construir uno para un script de Python:

1# Primero, crea una carpeta para tu proyecto
2mkdir mi-app-docker && cd mi-app-docker

Crea el directorio del proyecto

1# Archivo: app.py
2# Un script simple que procesa datos
3import json
4from datetime import datetime
5
6data = {
7 "app": "Mi primera app Docker",
8 "timestamp": datetime.now().isoformat(),
9 "python_version": "3.12",
10 "message": "¡Si ves esto, tu Dockerfile funciona perfecto!"
11}
12
13print(json.dumps(data, indent=2, ensure_ascii=False))

app.py — el script que empaquetaremos en Docker

1# Archivo: Dockerfile (sin extensión, exactamente este nombre)
2FROM python:3.12-slim
3
4WORKDIR /app
5
6COPY app.py .
7
8CMD ["python", "app.py"]

Tu primer Dockerfile — 4 líneas que empaquetan tu app

## Las instrucciones fundamentales del Dockerfile

  • FROM: "Empieza con esta imagen base". Siempre es la primera línea. Define los cimientos de tu imagen.
  • WORKDIR: "Trabaja dentro de este directorio". Crea el directorio si no existe y se posiciona ahí. Es como hacer cd pero dentro de la imagen.
  • COPY: "Copia archivos de tu ordenador al contenedor". COPY <origen-en-tu-PC> <destino-en-la-imagen>.
  • RUN: "Ejecuta este comando DURANTE la construcción de la imagen". Para instalar dependencias, crear directorios, etc.
  • CMD: "Ejecuta este comando CUANDO el contenedor arranque". Es el comando por defecto al hacer docker run.
  • ENV: "Define una variable de entorno". Disponible tanto durante la build como en runtime.
  • EXPOSE: "Documenta que este contenedor escucha en este puerto". Es informativo — no abre el puerto realmente.

## docker build: construir tu imagen

1# Construir la imagen (el punto "." indica el directorio actual)
2docker build -t mi-app:1.0 .
3
4# Desglose:
5# docker build → comando para construir
6# -t mi-app:1.0 → nombre:tag de tu imagen
7# . → directorio donde está el Dockerfile (contexto de build)
8
9# Verás algo como:
10# [1/3] FROM python:3.12-slim
11# [2/3] WORKDIR /app
12# [3/3] COPY app.py .
13# => naming to docker.io/library/mi-app:1.0
14
15# Verificar que la imagen existe
16docker images
17# REPOSITORY TAG IMAGE ID CREATED SIZE
18# mi-app 1.0 abc123def456 5 seconds ago 155MB
19
20# Ejecutar tu imagen personalizada
21docker run mi-app:1.0

Build + Run: de Dockerfile a contenedor ejecutándose

Consejo de senior: SIEMPRE versiona tus imágenes con tags numéricos (mi-app:1.0, mi-app:1.1, mi-app:2.0). Nunca uses solo "latest". Cuando algo se rompe en producción y necesitas volver a la versión anterior, agradecer tener tags es lo primero que harás.

## Un Dockerfile más realista: con dependencias

Los scripts del mundo real tienen dependencias (librerías externas). Veamos cómo incluirlas:

1# Archivo: requirements.txt
2requests==2.31.0
3pandas==2.1.4

requirements.txt — las dependencias de tu proyecto

1# Archivo: Dockerfile (versión con dependencias)
2FROM python:3.12-slim
3
4WORKDIR /app
5
6# Primero copiamos SOLO requirements.txt (truco de cache)
7COPY requirements.txt .
8RUN pip install --no-cache-dir -r requirements.txt
9
10# Luego copiamos el código
11COPY . .
12
13CMD ["python", "app.py"]

Dockerfile con dependencias — nota el orden de las instrucciones

¿Por qué copiamos requirements.txt ANTES que el resto del código? Por el CACHE de capas. Si cambias tu app.py pero no tocas requirements.txt, Docker reutiliza la capa de pip install del cache y solo recopia tu código. Esto ahorra MINUTOS en cada build. Si copiaras todo junto (COPY . .), cualquier cambio en cualquier archivo invalidaría el cache del pip install.

El orden de las instrucciones en un Dockerfile IMPORTA MUCHO para el rendimiento. Regla de oro: pon las cosas que cambian MENOS frecuentemente arriba (instalar dependencias del sistema, pip install) y las que cambian MÁS frecuentemente abajo (copiar tu código). Así maximizas el cache.

## .dockerignore: lo que NO quieres copiar

Igual que .gitignore le dice a Git qué archivos ignorar, .dockerignore le dice a Docker qué NO copiar al contexto de build. Esto hace la build más rápida y tu imagen más pequeña:

1# Archivo: .dockerignore
2__pycache__
3*.pyc
4.git
5.venv
6.env
7node_modules
8*.md
9.DS_Store

.dockerignore — mantén tu imagen limpia y ligera

Consejo de senior: si tu build es lenta, revisa el .dockerignore. He visto builds que tardaban 5 minutos porque copiaban la carpeta .git (con todo el historial), node_modules, o datasets de prueba de varios GB. El .dockerignore es la primera línea de defensa contra builds lentas.

## Multi-stage builds: imágenes limpias de producción

A veces necesitas herramientas pesadas para CONSTRUIR tu aplicación (compiladores, cabeceras de desarrollo, gcc) pero no las necesitas para EJECUTARLA. Un multi-stage build usa dos etapas: una para construir, otra para ejecutar. Solo la segunda acaba en la imagen final.

1# Etapa 1: construir (con herramientas pesadas)
2FROM python:3.12-slim AS builder
3WORKDIR /build
4COPY requirements.txt .
5RUN pip install --no-cache-dir --target=/build/deps -r requirements.txt
6
7# Etapa 2: ejecutar (sin herramientas de build)
8FROM python:3.12-slim
9WORKDIR /app
10COPY --from=builder /build/deps /usr/local/lib/python3.12/site-packages/
11COPY . .
12CMD ["python", "main.py"]

Multi-stage: la primera etapa instala, la segunda solo copia lo instalado.

Usarás multi-stage en el proyecto de esta sección. La idea siempre es la misma: construir en una etapa y copiar solo lo necesario a la etapa final. Si tu imagen pesa más de 500 MB, pregúntate si no estás arrastrando herramientas de build.

## No ejecutes como root: la instrucción USER

Por defecto, todo lo que corre dentro de un contenedor lo hace como root, el administrador. Si alguien encuentra un fallo en tu programa, tiene la llave de todo el contenedor. En producción se crea un usuario normal y se cambia a él con USER, al final del Dockerfile. Hay un detalle que muerde: las carpetas que crea Docker son de root, así que antes del USER hay que darle al usuario nuevo las que tenga que escribir, con chown («cambia el dueño»).

1FROM python:3.12-slim
2WORKDIR /app
3COPY . .
4
5# Crea el usuario, la carpeta de salida, y le da /app entera
6RUN useradd -m appuser && mkdir -p /app/output && chown -R appuser /app
7
8# A partir de aquí, todo corre como appuser
9USER appuser
10CMD ["python", "main.py"]

useradd crea el usuario, chown le da las carpetas, USER cambia a él

## Varias etiquetas para la misma imagen: docker tag

Con docker build -t le pones nombre y etiqueta a la imagen al construirla. Pero a menudo quieres que la misma imagen tenga varias etiquetas a la vez: la versión (1.2.0), la fecha (20240115) y latest, que es la que se usa si no dices ninguna. Para eso no hace falta reconstruir: docker tag le pone otra etiqueta a una imagen que ya existe. Una etiqueta no es una copia, es otro nombre para lo mismo, y por eso no ocupa disco.

1docker build -t mi-app:1.0 . # construye UNA vez
2docker tag mi-app:1.0 mi-app:latest # segunda etiqueta, sin reconstruir
3docker images mi-app # dos filas, el MISMO IMAGE ID

docker tag ORIGEN DESTINO: la imagen que ya tienes, y la etiqueta nueva

## Resumen del flujo completo

El flujo de trabajo con Dockerfiles es siempre el mismo: (1) Escribes un Dockerfile describiendo tu entorno. (2) Ejecutas docker build para crear una imagen. (3) Ejecutas docker run para crear un contenedor a partir de esa imagen. (4) Si cambias algo, vuelves a hacer build (el cache hace que sea rápido). Es un ciclo que repetirás cientos de veces en tu carrera.

Regístrate para guardar tu progreso.