Docker Compose para desarrollo local: una guía completa
Cómo montar un entorno de desarrollo local productivo con Docker Compose, con recarga en caliente, persistencia de base de datos y paridad con producción.

"Pero en mi máquina sí funciona" dejó de ser aceptable cuando Docker resolvió el problema de paridad entre entornos. Aun así, muchos equipos aún tienen un README de 15 pasos para levantar el entorno local, distintas versiones de Node en cada laptop y una versión de PostgreSQL en desarrollo que no coincide con la de producción.
Docker Compose envuelve todo tu stack de desarrollo — aplicación, base de datos, caché, cola — en un solo comando docker compose up. El reto es hacerlo sin sacrificar la experiencia del desarrollador. Nadie usará una configuración que tarda 2 minutos en reconstruirse después de cambiar una línea de código.
La configuración base
Empieza con un docker-compose.yml que refleje tu infraestructura de producción. Cada servicio del que dependa tu aplicación tiene su propio contenedor.
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://devuser:devpass@db:5432/appdb
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d appdb"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:Decisiones clave en esta configuración:
- Volumen con nombre para PostgreSQL —
pgdatapersiste los datos entre reinicios. Sin él, pierdes la base de datos cada vez que ejecutasdocker compose down. - Health checks en las dependencias —
depends_onconcondition: service_healthyevita que la aplicación arranque antes de que la base de datos esté lista. - Mapeo de puertos — Expón puertos para acceder directamente a la base de datos con herramientas como pgAdmin o TablePlus.
Dockerfile de desarrollo
El Dockerfile de desarrollo difiere del de producción. Prioriza la velocidad de reconstrucción y la recarga en caliente por encima del tamaño de imagen y la seguridad.
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
# Install dependencies first — cached unless package.json changes
COPY package.json package-lock.json ./
RUN npm ci
# Don't copy source files — they're mounted as a volume
# This means changes are reflected immediately
EXPOSE 3000
CMD ["npm", "run", "dev"]# ❌ Anti-pattern — COPY everything, then install
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
CMD ["npm", "run", "dev"]
# Every source file change invalidates the npm ci cache
# ✅ Layer ordering — dependencies cached separately
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Source files come from the volume mount, not COPY
CMD ["npm", "run", "dev"]El montaje de volumen (- .:/app) mapea tu código fuente local dentro del contenedor. El segundo volumen (- /app/node_modules) evita que el node_modules local sobrescriba el del contenedor — podrían tener binarios específicos de la plataforma.
Configuración de recarga en caliente
La recarga en caliente dentro de Docker requiere que el observador de archivos detecte los cambios desde el montaje de volumen. Algunas herramientas necesitan configuración explícita para esto.
// next.config.js — Next.js in Docker
module.exports = {
webpack: (config) => {
// Enable polling-based file watching for Docker volumes
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
};
return config;
},
};Para herramientas que usan chokidar (Vite, nodemon, webpack-dev-server):
{
"scripts": {
"dev": "CHOKIDAR_USEPOLLING=true next dev"
}
}El polling consume ligeramente más CPU que los eventos nativos del sistema de archivos, pero es la única opción confiable para montajes de volumen de Docker en macOS y Windows.
Sobrescrituras específicas por entorno
Usa docker-compose.override.yml para ajustes específicos de cada desarrollador que no deban versionarse. Docker Compose la fusiona automáticamente con el archivo base.
# docker-compose.override.yml (gitignored)
services:
app:
environment:
- DEBUG=app:*
- LOG_LEVEL=debug
ports:
- "9229:9229" # Node.js debugger
db:
ports:
- "5433:5432" # Custom port to avoid conflictsPara pruebas parecidas a producción, usa un archivo de sobrescritura explícito:
# docker-compose.prod.yml
services:
app:
build:
dockerfile: Dockerfile
environment:
- NODE_ENV=production
volumes: [] # No source mount — use built image# Development (default)
docker compose up
# Production-like testing
docker compose -f docker-compose.yml -f docker-compose.prod.yml upGestión de la base de datos
Las bases de datos de desarrollo necesitan seeding, migraciones y resets ocasionales. Automatiza esto con comandos de compose.
# docker-compose.yml — add utility services
services:
migrate:
build:
context: .
dockerfile: Dockerfile.dev
command: npx prisma migrate deploy
environment:
- DATABASE_URL=postgresql://devuser:devpass@db:5432/appdb
depends_on:
db:
condition: service_healthy
profiles:
- tools
seed:
build:
context: .
dockerfile: Dockerfile.dev
command: npx prisma db seed
environment:
- DATABASE_URL=postgresql://devuser:devpass@db:5432/appdb
depends_on:
db:
condition: service_healthy
profiles:
- tools# Run migrations
docker compose --profile tools run --rm migrate
# Seed the database
docker compose --profile tools run --rm seed
# Reset everything — database, volumes, containers
docker compose down -v
docker compose up -d
docker compose --profile tools run --rm migrate
docker compose --profile tools run --rm seedLa opción profiles: [tools] significa que estos servicios no arrancan con docker compose up. Solo se ejecutan cuando se invocan explícitamente — manteniendo el arranque por defecto limpio.
Depuración dentro de los contenedores
Conecta un depurador al proceso de Node.js que corre dentro de Docker exponiendo el puerto de debug y configurando tu IDE.
# docker-compose.yml
services:
app:
command: node --inspect=0.0.0.0:9229 node_modules/.bin/next dev
ports:
- "3000:3000"
- "9229:9229"// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Docker: Attach",
"type": "node",
"request": "attach",
"port": 9229,
"remoteRoot": "/app",
"localRoot": "${workspaceFolder}",
"restart": true
}
]
}La bandera --inspect=0.0.0.0:9229 vincula el depurador a todas las interfaces dentro del contenedor (no solo a localhost), haciéndolo accesible desde el host.
Errores comunes
# ❌ Hardcoded secrets in docker-compose.yml
services:
db:
environment:
POSTGRES_PASSWORD: my-real-password-123
# ✅ Use .env file (gitignored)
services:
db:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}# .env (add to .gitignore)
DB_PASSWORD=local-dev-only-passwordOtros errores a evitar:
- Falta de volumen para node_modules — los módulos del host sobrescriben los del contenedor, causando errores específicos de plataforma
- Sin health checks en las bases de datos — la aplicación arranca antes de que la base de datos esté lista y falla en la primera consulta
- Usar etiquetas
latest—postgres:latesthoy no espostgres:latestmañana. Fija las versiones.
Conclusiones clave
- Replica la infraestructura de producción — usa la misma versión de base de datos, mismos servicios, misma configuración
- Separa la instalación de dependencias del código fuente — el orden de capas del Dockerfile mantiene las reconstrucciones rápidas
- Monta el código fuente como volumen para la recarga en caliente — no copies archivos fuente en desarrollo
- Usa health checks en las dependencias —
depends_onsin condiciones no espera a que estén listas - Usa profiles para servicios de utilidad — las migraciones y seeds no deben arrancar automáticamente
- Fija las versiones de las imágenes —
postgres:16-alpine, nopostgres:latest


