🎯 Meta: empaquetar cualquier aplicación para que corra igual en tu máquina, en la del compañero y en el servidor de producción. Docker resuelve el eterno "en mi máquina funciona". Es la herramienta más importante de DevOps moderno.
Versión: Docker Engine 28.x · Docker Compose v2.
12.1 · El problema que resuelve Docker
Sin Docker: Con Docker:
"En mi máquina funciona" "Funciona en el contenedor"
Tú: PHP 8.5, Postgres 18 La imagen lleva TODO dentro:
Server: PHP 8.1, Postgres 15 💥 código + PHP 8.5 + libs + config
Compañero: Windows, tú Mac 💥 → corre idéntico en cualquier lado ✅Un contenedor empaqueta tu app con todo lo que necesita (runtime, librerías, config) en una unidad que corre igual en cualquier máquina con Docker. No es una máquina virtual: comparte el kernel del sistema, por eso es ligero y rápido (arranca en milisegundos, pesa MB no GB).
Máquina Virtual Contenedor
┌──────────────────┐ ┌──────────────────┐
│ App │ │ App │
│ Librerías │ │ Librerías │
│ SO INVITADO │ ← pesado │ (comparte kernel)│ ← ligero
│ completo (GB) │ └──────────────────┘
├──────────────────┤ ┌──────────────────┐
│ Hypervisor │ │ Docker Engine │
├──────────────────┤ ├──────────────────┤
│ SO anfitrión │ │ SO anfitrión │
└──────────────────┘ └──────────────────┘12.2 · Conceptos fundamentales
IMAGEN → la "plantilla" de solo lectura (tu app empaquetada). Como una clase.
CONTENEDOR → una instancia en ejecución de una imagen. Como un objeto.
DOCKERFILE → la receta para construir una imagen.
REGISTRY → almacén de imágenes (Docker Hub, GitHub Container Registry).
VOLUMEN → almacenamiento persistente (los datos sobreviven al contenedor).
RED (network) → cómo se comunican los contenedores entre sí.🧠 Imagen vs Contenedor: la imagen es la receta + ingredientes congelados; el contenedor es el plato cocinado y servido. De una imagen lanzas muchos contenedores idénticos.
Comandos básicos:
docker pull postgres:18 # descarga una imagen
docker run postgres:18 # crea y arranca un contenedor
docker ps # contenedores corriendo
docker ps -a # todos (incluidos parados)
docker images # imágenes locales
docker logs <id> # ver los logs de un contenedor
docker exec -it <id> bash # entrar a un contenedor en marcha
docker stop <id> / docker rm <id> # parar / eliminar
docker system prune # limpiar basura (imágenes/contenedores sueltos)12.3 · Tu primer Dockerfile
Un Dockerfile describe cómo construir la imagen de tu app. Ejemplo para FastAPI (Python):
# Imagen base: Python 3.13 ligero
FROM python:3.13-slim
# Directorio de trabajo dentro del contenedor
WORKDIR /app
# Copiar SOLO las dependencias primero (aprovecha la caché de capas)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar el resto del código
COPY . .
# Puerto que expone la app
EXPOSE 8000
# Comando al arrancar
CMD ["gunicorn", "main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]docker build -t tienda-api . # construir la imagen (el "." es el contexto)
docker run -p 8000:8000 tienda-api # ejecutar, mapeando el puerto💡 Tip de oro (orden de capas): copia
requirements.txte instala antes de copiar el código. Docker cachea cada instrucción; si solo cambias tu código, no reinstala las dependencias (que es lo lento). Este truco te ahorra minutos en cada build. Aplica igual apackage.json(Node),composer.json(PHP),go.mod(Go).
Instrucciones de Dockerfile más usadas
| Instrucción | Qué hace |
|---|---|
FROM | Imagen base de la que partes |
WORKDIR | Carpeta de trabajo dentro del contenedor |
COPY / ADD | Copiar archivos al contenedor |
RUN | Ejecutar un comando al construir (instalar deps) |
ENV | Variable de entorno |
EXPOSE | Documentar qué puerto usa (informativo) |
CMD | Comando por defecto al arrancar el contenedor |
ENTRYPOINT | Comando fijo (CMD le pasa argumentos) |
12.4 · Multi-stage builds (imágenes pequeñas)
Compilas en una imagen "gorda" y copias solo el resultado a una imagen mínima. Brilla con Go:
# ─── Etapa 1: compilar ───
FROM golang:1.26 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o servidor .
# ─── Etapa 2: imagen final mínima ───
FROM scratch # imagen VACÍA (0 bytes de base)
COPY --from=builder /app/servidor /servidor
EXPOSE 8080
ENTRYPOINT ["/servidor"]Resultado: una imagen de ~10 MB (solo el binario), en vez de ~800 MB. Menos peso = despliegues más rápidos, menos superficie de ataque, menos coste.
Para NestJS/Node el patrón multi-stage también aplica:
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev # solo dependencias de producción
COPY --from=build /app/dist ./dist
CMD ["node", "dist/main.js"]Para Bun/Elysia, la base es oven/bun:1 y arranca en milisegundos.
💡 Usa imágenes
slimoalpinesiempre que puedas. Alpine es Linux minimalista (~5 MB). Solo cuidado: alpine usamuslen vez deglibc, lo que a veces da problemas con librerías nativas de Python. Si algo raro pasa, prueba la varianteslim.
12.5 · Docker Compose (varios contenedores juntos)
Una app real tiene varias piezas: la API, la base de datos, quizá Redis, Nginx… Docker Compose las orquesta con un solo archivo compose.yaml:
# compose.yaml
services:
api:
build: . # construye desde el Dockerfile local
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://postgres:secreto@db:5432/tienda
depends_on:
db:
condition: service_healthy # espera a que la BD esté lista
restart: unless-stopped
db:
image: postgres:18
environment:
POSTGRES_PASSWORD: secreto
POSTGRES_DB: tienda
volumes:
- datos_db:/var/lib/postgresql/data # persiste los datos
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
datos_db: # volumen nombrado (persistente)docker compose up -d # levanta todo en segundo plano
docker compose logs -f api # ver logs de un servicio
docker compose ps # estado
docker compose down # parar y eliminar (los volúmenes quedan)
docker compose down -v # ...incluyendo volúmenes (borra datos!)🧠 La magia de las redes de Compose: los servicios se ven entre sí por su nombre. La API se conecta a la BD con el host
db(nolocalhost), porque Compose crea una red interna donde cada servicio es un hostname. Por eso elDATABASE_URLdice@db:5432.
12.6 · Volúmenes: no pierdas tus datos
Un contenedor es efímero: si lo borras, se va todo lo que había dentro. Para que los datos sobrevivan (la BD, archivos subidos) usas volúmenes:
volumes:
- datos_db:/var/lib/postgresql/data # volumen nombrado (gestionado por Docker)
- ./uploads:/app/uploads # bind mount (carpeta de tu máquina)- Volumen nombrado (
datos_db:): Docker lo gestiona, ideal para BD en producción. - Bind mount (
./carpeta:): mapea una carpeta real de tu disco, útil en desarrollo (editas el código y se refleja dentro del contenedor al instante).
⚠️ El error que todos cometen una vez: correr PostgreSQL en Docker sin volumen, meter datos importantes, borrar el contenedor y… adiós datos. Siempre volumen para cualquier cosa que deba persistir.
12.7 · Variables de entorno y secretos
Nunca metas contraseñas en el Dockerfile ni en la imagen. Usa variables de entorno:
services:
api:
env_file:
- .env # carga variables desde un archivo
environment:
APP_ENV: production # o directamente aquí⚠️ Los secretos NO van en la imagen. Cualquiera que baje tu imagen puede inspeccionar sus capas y ver un secreto "quemado" dentro. Pásalos en runtime (env vars) o usa Docker Secrets / el gestor de secretos de tu nube. Y el
.envfuera de git (cap. 00).
12.8 · Buenas prácticas de Docker (checklist)
- Imagen base oficial y con versión fija:
python:3.13-slim, nopython:latest.latestcambia sin avisar y rompe builds reproducibles. - Multi-stage para imágenes pequeñas.
.dockerignorepara no copiar basura (node_modules, .git, .env) a la imagen:node_modules .git .env __pycache__ *.log- Un proceso por contenedor. No metas API + BD + Nginx en el mismo contenedor.
- Usuario no-root dentro del contenedor (seguridad):dockerfile
RUN adduser --disabled-password appuser USER appuser - Healthchecks para que Docker/orquestador sepa si tu app está viva.
- Ordena las capas de menos a más cambiante (deps antes que código).
- No guardes datos en el contenedor: usa volúmenes.
12.9 · Escaneo de vulnerabilidades e imágenes seguras
Una imagen python:3.13-slim de hace 6 meses puede arrastrar librerías del sistema con CVEs ya conocidos. Escanéala antes de subirla a producción, igual que escaneas dependencias de tu app (apéndice C):
# Trivy — el escáner de imágenes más usado en 2026, gratis y rápido
trivy image tienda-api:latest
# Falla el pipeline si hay vulnerabilidades CRITICAL sin parchear
trivy image --severity CRITICAL --exit-code 1 tienda-api:latest# En CI (cap. 14), justo después de docker build
- name: Escanear imagen
run: trivy image --severity HIGH,CRITICAL --exit-code 1 ghcr.io/miapp/api:${{ github.sha }}💡 Genera también un SBOM (Software Bill of Materials — el "listado de ingredientes" de tu imagen: qué paquetes y versiones exactas contiene) con
trivy image --format cyclonedxodocker sbom. Cuando aparezca un CVE nuevo mañana, puedes preguntar "¿alguna de mis imágenes en producción lo tiene?" sin volver a escanear todo desde cero.
⚠️ Reconstruye imágenes viejas aunque tu código no haya cambiado. Si tu
DockerfilefijaFROM python:3.13-slimy nunca reconstruyes, sigues sirviendo los parches de seguridad del sistema operativo del día que la construiste, no los de hoy. Programa reconstrucciones periódicas (semanal) igual que programas backups.
12.10 · De Docker a producción (un vistazo)
- Un servidor (VPS):
docker compose up -ddetrás de Nginx (cap. 13). Suficiente para la mayoría de proyectos. - Escala grande: orquestadores como Kubernetes (k8s) gestionan cientos de contenedores en muchas máquinas: auto-escalado, auto-reparación, despliegues sin downtime. Es potente y complejo; no lo necesitas hasta que lo necesitas de verdad.
- Nube gestionada: servicios como Google Cloud Run, AWS ECS/Fargate o Railway corren tus contenedores sin que administres servidores.
🧠 Consejo: domina Docker + Compose + un VPS con Nginx primero. Cubre el 90% de los casos. Kubernetes es una herramienta de escala que añade muchísima complejidad; apréndela cuando un proyecto real lo pida, no "por CV".
✅ Ejercicio del capítulo
Dockeriza una de tus APIs del blog, completa:
1. Dockerfile multi-stage optimizado (caché de dependencias bien ordenada).
2. .dockerignore correcto.
3. Usuario no-root dentro del contenedor.
4. compose.yaml con: tu api + PostgreSQL (con volumen) + healthcheck.
5. La API se conecta a la BD por el nombre del servicio (no localhost).
6. Variables sensibles vía .env (fuera de git).
7. `docker compose up -d` y comprueba que la API responde y los datos
persisten tras `docker compose down` y volver a levantar.
8. Mide el tamaño final de la imagen y optimízalo (base slim/alpine).Tu app ahora corre igual en cualquier máquina del planeta. Falta ponerle un buen portero delante: Nginx.
💡 Pistas de la solución
- Si el build tarda igual al cambiar solo tu código, revisa el orden de las instrucciones:
COPY requirements.txt ./RUN pip install...debe ir ANTES deCOPY . .— si están al revés, Docker invalida la caché de dependencias en cada cambio de código. - Para verificar que la API usa el nombre del servicio y no
localhost: apaga temporalmente el volumen de datos, borra el contenedor de la API y vuelve a levantarlo — si sigue conectando a la BD sin cambiar código, el hostname está bien resuelto por Compose. - El tamaño de la imagen se mide con
docker images— compara antes/después de pasar a multi-stage yslim/alpine. Una reducción de 10× (de ~800MB a ~80MB) es normal en Node; en Go, de ~800MB a ~10MB conscratch.
🧠 Autoevaluación
Docker cachea cada instrucción del Dockerfile por capas. Si el archivo de dependencias no cambió, la instrucción RUN npm install/pip install reutiliza la capa cacheada aunque el resto del código sí haya cambiado — evitando reinstalar todo en cada build.
Un contenedor es efímero: todo lo que se escribe dentro de su sistema de archivos desaparece al borrarlo. Sin un volumen que persista /var/lib/postgresql/data fuera del ciclo de vida del contenedor, borrar (o recrear) el contenedor de la BD borra todos los datos sin aviso.
Compose crea una red interna donde cada servicio es accesible por su nombre — "db" resuelve a la IP del contenedor de PostgreSQL dentro de esa red. "localhost" dentro del contenedor de la API se refiere al propio contenedor de la API, no al de la base de datos.
La primera etapa incluye todo el toolchain de compilación de Go (pesado); la segunda etapa parte de una imagen mínima (incluso scratch, vacía) y copia SOLO el binario compilado. El resultado final no lleva compilador, código fuente ni dependencias de build — solo el ejecutable, típicamente unos pocos MB frente a los cientos del entorno de compilación.
No necesariamente: python:3.13-slim es una etiqueta fija en tu Dockerfile, pero el sistema operativo base dentro de esa imagen puede acumular CVEs conocidos que no se parchean solos — cada build reutiliza la capa base tal como estaba cuando la descargaste. Hay que reconstruir (y volver a escanear con algo como Trivy) periódicamente, aunque tu propio código no haya cambiado una línea.
Siguiente: 13-nginx-apache.md — el servidor web que recibe al mundo.