Llegamos al cierre de la serie Docker Compose de cero a producción. En la Parte 1 vimos el archivo compose.yaml, en la Parte 2 los comandos y las dependencias robustas. Ahora lo llevamos a la práctica: dos proyectos reales completos y la estrategia que uso para manejar configuraciones distintas entre desarrollo y producción.
Proyecto 1: WordPress + MySQL
El ejemplo clásico de un stack de dos niveles: una aplicación web y su base de datos. Este proyecto completo vive en un solo archivo.
Estructura del proyecto
wordpress_project/
└── compose.yaml
compose.yaml
services:
db:
image: mysql:8.0
container_name: wordpress_db
restart: unless-stopped
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp_password
MYSQL_ROOT_PASSWORD: root_secret_password
volumes:
- db_data:/var/lib/mysql
networks:
- app_network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
wordpress:
image: wordpress:latest
container_name: wordpress_app
restart: unless-stopped
depends_on:
db:
condition: service_healthy # Espera que MySQL esté listo de verdad
ports:
- "8000:80"
environment:
WORDPRESS_DB_HOST: db:3306 # 'db' es el nombre del servicio = hostname
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp_password
WORDPRESS_DB_NAME: wordpress
networks:
- app_network
networks:
app_network:
driver: bridge
volumes:
db_data:
driver: local
Puesta en marcha
cd wordpress_project
docker compose up -d
# Esperar unos segundos y visitar http://localhost:8000
# El instalador de WordPress estará listo
Lo que me parece elegante de este ejemplo: WORDPRESS_DB_HOST: db:3306. No hay IP, no hay configuración de DNS manual. El nombre del servicio db funciona directamente como hostname gracias a la red que Compose crea automáticamente. Esto es lo que hace que los entornos sean tan portables.
Proyecto 2: API REST con Node.js y PostgreSQL
Este ejemplo es más representativo del trabajo diario: construimos nuestra propia imagen desde un Dockerfile y configuramos un entorno de desarrollo con hot-reload.
Estructura del proyecto
api_project/
├── compose.yaml
├── Dockerfile
├── index.js
└── package.json
Dockerfile
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
index.js
const express = require('express');
const { Pool } = require('pg');
const app = express();
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST, // Viene del ambiente: 'db'
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: 5432,
});
app.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT NOW()');
res.send(`Hora en la base de datos: ${result.rows[0].now}`);
} catch (err) {
res.status(500).send('Error conectando a la base de datos');
}
});
app.listen(3000, () => console.log('API corriendo en http://localhost:3000'));
compose.yaml
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: apidb
POSTGRES_USER: apiuser
POSTGRES_PASSWORD: apipassword
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U apiuser -d apidb"]
interval: 10s
timeout: 5s
retries: 5
api:
build: .
ports:
- "3000:3000"
volumes:
- .:/usr/src/app # Bind mount: cambios en el código se reflejan al instante
environment:
DB_HOST: db
DB_USER: apiuser
DB_PASSWORD: apipassword
DB_NAME: apidb
depends_on:
db:
condition: service_healthy
volumes:
pg_data: {}
# La primera vez, --build para construir la imagen
docker compose up --build -d
# Verificar que funciona
curl http://localhost:3000
# → Hora en la base de datos: 2025-05-01 14:23:45.123456+00
El bind mount (- .:/usr/src/app) es el truco de desarrollo: cualquier cambio en el código se refleja dentro del contenedor sin reconstruir la imagen. Para que los cambios se apliquen automáticamente en Node.js, agregá nodemon como dependencia de desarrollo y usalo en el CMD.
Gestión de entornos: desarrollo vs producción
Esta fue una de las lecciones más importantes que aprendí: nunca uses el mismo archivo compose.yaml sin modificaciones para desarrollo y producción. Las necesidades son opuestas: en desarrollo querés agilidad y debuggear fácil; en producción, estabilidad, seguridad y recursos bien definidos.
Estrategia con compose.override.yaml
Compose tiene un mecanismo automático: si existe un compose.override.yaml en el mismo directorio, lo fusiona automáticamente con el compose.yaml base al ejecutar docker compose up.
# compose.yaml — Base (versionado en Git)
services:
api:
build: .
restart: unless-stopped
environment:
- APP_ENV=production
# compose.override.yaml — Desarrollo (puede NO versionarse si tiene configs locales)
services:
api:
ports:
- "8000:8000"
- "9229:9229" # Puerto de debugging de Node.js
volumes:
- .:/app # Bind mount para hot-reload
environment:
- APP_ENV=development # Sobreescribe el valor del base
# Servicio adicional solo para desarrollo
pgadmin:
image: dpage/pgadmin4
ports:
- "5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: dev@local.com
PGADMIN_DEFAULT_PASSWORD: dev
Estrategia con múltiples archivos explícitos
# compose.prod.yaml — Producción
services:
api:
restart: always
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Desarrollo (usa override automático)
docker compose up -d
# Producción (combina base + prod explícitamente)
docker compose -f compose.yaml -f compose.prod.yaml up -d
Archivos .env para variables de entorno
Para evitar hardcodear valores en los archivos YAML (especialmente secretos), uso archivos .env. Compose los carga automáticamente si existen en el mismo directorio.
# .env (NO versionar si tiene contraseñas reales — agregarlo al .gitignore)
TAG=15-alpine
POSTGRES_USER=miusuario
POSTGRES_PASSWORD=supersecreto
POSTGRES_DB=miapp
# compose.yaml usando las variables del .env
services:
db:
image: postgres:${TAG}
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
Cada desarrollador tiene su propio .env local con sus credenciales. El archivo compose.yaml se versiona sin datos sensibles. Para producción, las variables se inyectan desde el CI/CD o desde un gestor de secretos.
Builds multi-stage para producción
Las imágenes de desarrollo son gordas (incluyen herramientas de build, dependencias de dev, etc.). Las de producción deben ser mínimas. Los builds multi-stage resuelven esto con un solo Dockerfile:
# Dockerfile
# ---- Etapa de Build ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
# ---- Etapa de Producción ----
FROM node:20-slim AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
# compose.yaml
services:
api:
build:
context: .
target: production # Solo construye hasta la etapa 'production'
La imagen de producción solo contiene lo necesario para correr — sin el compilador, sin herramientas de build, sin dependencias de desarrollo. Más pequeña, más segura, más rápida de desplegar.
El checklist final para ir a producción con Compose
- ✅
restart: unless-stoppedoalwaysen todos los servicios críticos - ✅ Healthchecks definidos en las bases de datos y servicios de los que dependen otros
- ✅
depends_onconcondition: service_healthydonde corresponde - ✅ Volúmenes nombrados para cualquier dato que deba persistir
- ✅ Secretos y contraseñas en
.envo variables de CI/CD, nunca hardcodeados - ✅ Imágenes multi-stage para producción
- ✅
deploy.resources.limitsconfigurados para evitar que un servicio consuma todo el host - ✅ Logging configurado (
json-fileconmax-sizepara evitar llenar el disco)
¿Y después de Compose?
Docker Compose es ideal para un único host. Cuando necesitás escalar a múltiples máquinas, alta disponibilidad real o gestión de carga compleja, los próximos pasos naturales son:
- Docker Swarm: orquestación multi-host con conceptos muy similares a Compose. Curva de aprendizaje baja si ya dominás Compose.
- Kubernetes: el estándar de la industria para producción a gran escala. La herramienta
kompose converttraduce tus archivos Compose a manifiestos de Kubernetes para facilitar la migración.
Lo bueno: los conceptos que aprendiste en esta serie (servicios, redes, volúmenes, variables de entorno, healthchecks) se trasladan directamente a esos entornos.
← Parte 2: CLI esencial, depends_on, healthchecks y perfiles