Docker Compose para el desarrollo: infraestructura local
La diferencia entre una experiencia de incorporación agradable ("clonar y ejecutar pnpm dev:infra") y una dolorosa ("primero configurar PostgreSQL, luego configurar Redis, luego...") se reduce a qué tan bien la configuración de Docker Compose captura sus requisitos de infraestructura. Un docker-compose.dev.yml bien diseñado permite a cualquier desarrollador en cualquier máquina tener exactamente la misma infraestructura ejecutándose en minutos.
Esta guía cubre los patrones para una pila de desarrollo local con calidad de producción: configuración de servicios, controles de estado, redes, administración de volúmenes e integración con la secuencia de inicio de su aplicación.
Conclusiones clave
- Utilice un puerto no predeterminado para PostgreSQL localmente (5433) para evitar conflictos con las instalaciones del sistema.
- Las comprobaciones de estado de las dependencias del servicio evitan errores de inicio de "conexión rechazada"
- Los volúmenes con nombre conservan los datos de la base de datos entre reinicios del contenedor; los montajes de enlace no funcionan de manera confiable en Windows
- Utilice
env_filepara cargar variables de entorno desde su archivo.env.localen contenedores- Separe
docker-compose.dev.ymldedocker-compose.prod.yml: tienen diferentes propósitos- El patrón
depends_on.condition: service_healthyespera la preparación real, no solo el inicio del contenedor- Utilice
profilespara optar por servicios opcionales (correo electrónico, monitoreo)- Ejecute
docker compose(v2), nodocker-compose(v1): la sintaxis del complemento está actualizada
La pila de desarrollo completa
# infrastructure/docker-compose.dev.yml
name: ecosire-dev
services:
# ─── PostgreSQL ─────────────────────────────────────────────────
postgres:
image: postgres:17-alpine
container_name: ecosire-postgres
environment:
POSTGRES_DB: ecosire_dev
POSTGRES_USER: ecosire
POSTGRES_PASSWORD: dev_password_change_in_prod
ports:
- "5433:5432" # 5433 externally — avoids conflicts with system PostgreSQL
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d # Run SQL on first start
command: >
postgres
-c shared_buffers=256MB
-c effective_cache_size=1GB
-c work_mem=16MB
-c maintenance_work_mem=128MB
-c checkpoint_completion_target=0.9
-c wal_buffers=16MB
-c max_connections=100
-c log_min_duration_statement=100
-c log_statement=ddl
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ecosire -d ecosire_dev"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
# ─── Redis ──────────────────────────────────────────────────────
redis:
image: redis:7-alpine
container_name: ecosire-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: >
redis-server
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--appendonly yes
--appendfsync everysec
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
# ─── Authentik (Identity Provider) ──────────────────────────────
authentik-server:
image: ghcr.io/goauthentik/server:2024.12
container_name: ecosire-authentik
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgres
AUTHENTIK_POSTGRESQL__USER: ecosire
AUTHENTIK_POSTGRESQL__PASSWORD: dev_password_change_in_prod
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_SECRET_KEY: dev-secret-key-change-in-production-32chars
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
AUTHENTIK_DISABLE_STARTUP_ANALYTICS: "true"
volumes:
- authentik_media:/media
- authentik_certs:/certs
ports:
- "9000:9000" # HTTP
- "9443:9443" # HTTPS
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "ak healthcheck"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s # Authentik takes time to initialize
restart: unless-stopped
authentik-worker:
image: ghcr.io/goauthentik/server:2024.12
container_name: ecosire-authentik-worker
command: worker
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgres
AUTHENTIK_POSTGRESQL__USER: ecosire
AUTHENTIK_POSTGRESQL__PASSWORD: dev_password_change_in_prod
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_SECRET_KEY: dev-secret-key-change-in-production-32chars
volumes:
- authentik_media:/media
- authentik_certs:/certs
- /var/run/docker.sock:/var/run/docker.sock # For Authentik's proxy
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
# ─── Mailpit (Email Testing) ─────────────────────────────────────
mailpit:
image: axllent/mailpit:latest
container_name: ecosire-mailpit
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
environment:
MP_MAX_MESSAGES: 200
MP_SMTP_AUTH_ACCEPT_ANY: true
MP_SMTP_AUTH_ALLOW_INSECURE: true
restart: unless-stopped
profiles:
- email # Optional — use `docker compose --profile email up`
# ─── pgAdmin (Database GUI) ─────────────────────────────────────
pgadmin:
image: dpage/pgadmin4:latest
container_name: ecosire-pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: [email protected]
PGADMIN_DEFAULT_PASSWORD: admin
PGADMIN_CONFIG_SERVER_MODE: "False"
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
ports:
- "5050:80"
volumes:
- pgadmin_data:/var/lib/pgadmin
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
profiles:
- tools # Optional
networks:
default:
name: ecosire-dev-network
volumes:
postgres_data:
name: ecosire-postgres-data
redis_data:
name: ecosire-redis-data
authentik_media:
name: ecosire-authentik-media
authentik_certs:
name: ecosire-authentik-certs
pgadmin_data:
name: ecosire-pgadmin-data
Secuencias de comandos del paquete.json
Conecte los comandos de Docker Compose a sus scripts monorepo:
{
"scripts": {
"dev:infra": "docker compose -f infrastructure/docker-compose.dev.yml up -d",
"dev:infra:down": "docker compose -f infrastructure/docker-compose.dev.yml down",
"dev:infra:logs": "docker compose -f infrastructure/docker-compose.dev.yml logs -f",
"dev:infra:reset": "docker compose -f infrastructure/docker-compose.dev.yml down -v && pnpm dev:infra",
"dev:infra:email": "docker compose -f infrastructure/docker-compose.dev.yml --profile email up -d",
"dev:infra:tools": "docker compose -f infrastructure/docker-compose.dev.yml --profile tools up -d"
}
}
El indicador --profile permite que los servicios opcionales (pruebas de correo electrónico con Mailpit, GUI de base de datos con pgAdmin) permanezcan inactivos hasta que se soliciten explícitamente.
Scripts de inicialización de bases de datos
Coloque los archivos SQL en infrastructure/init-scripts/; se ejecutan en el primer inicio del contenedor:
-- infrastructure/init-scripts/01-create-databases.sql
-- Create all databases Authentik needs separately from the app DB
CREATE DATABASE authentik;
GRANT ALL PRIVILEGES ON DATABASE authentik TO ecosire;
-- Create test database for CI
CREATE DATABASE ecosire_test;
GRANT ALL PRIVILEGES ON DATABASE ecosire_test TO ecosire;
-- infrastructure/init-scripts/02-extensions.sql
-- Enable PostgreSQL extensions
\c ecosire_dev;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Trigram search
CREATE EXTENSION IF NOT EXISTS "btree_gin"; -- Composite GIN indexes
\c ecosire_test;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
Los scripts de inicialización se ejecutan en orden alfabético. El metacomando \c database_name psql cambia la base de datos activa.
Integración de variables de entorno
Su aplicación lee desde .env.local en la raíz de monorepo. Los servicios de Docker necesitan saber cómo conectarse entre sí mediante nombres de servicios (no localhost):
# .env.local (monorepo root)
# PostgreSQL — use 5433 externally (host) or 5432 internally (container network)
DATABASE_URL=postgresql://ecosire:dev_password_change_in_prod@localhost:5433/ecosire_dev
# Redis
REDIS_URL=redis://localhost:6379
# Authentik — use 9000 for external calls from your dev machine
AUTHENTIK_URL=http://localhost:9000
# Use service name for server-to-server calls within Docker network
AUTHENTIK_INTERNAL_URL=http://authentik-server:9000
# Email (Mailpit SMTP)
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_SECURE=false
# Application
NODE_ENV=development
Para aplicaciones que se ejecutan dentro de Docker y que necesitan comunicarse con otros servicios, utilice nombres de servicios. Para aplicaciones que se ejecutan en su máquina host (NestJS, Next.js en modo de desarrollo), use localhost con los puertos asignados al host.
Análisis profundo de controles de salud
Las comprobaciones de estado evitan fallos de inicio en cascada. El depends_on.condition: service_healthy espera la preparación real, no solo el inicio del contenedor:
# Without health checks — can fail because PostgreSQL isn't ready
depends_on:
- postgres
# With health checks — waits for PostgreSQL to accept connections
depends_on:
postgres:
condition: service_healthy
Controles de salud personalizados para sus propios servicios:
// apps/api/src/health/health.controller.ts
@Get()
@Public()
@HealthCheck()
async check() {
return this.health.check([
() => this.db.isHealthy('database'),
() => this.redis.isHealthy('redis'),
]);
}
# If your API is also dockerized
api:
image: ecosire-api:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
Gestión de volumen
Los volúmenes con nombre conservan los datos entre reinicios. Comprenda cuándo utilizar cada tipo de volumen:
| Tipo | Persistencia | Rendimiento | Usar para |
|---|---|---|---|
| Volumen nombrado | Sí | Excelente | Datos de la base de datos |
| Montaje de enlace | Sí | Bueno (Linux), Malo (macOS) | Recarga en caliente del código fuente |
| tmpfs | No | Excelente | Archivos temporales, secretos |
# Use bind mounts for source code (enables hot reload)
volumes:
- ./apps/api/src:/app/src # Code changes reflected immediately
# Use named volumes for data
volumes:
- postgres_data:/var/lib/postgresql/data
# Use tmpfs for ephemeral data
volumes:
- type: tmpfs
target: /tmp
En macOS con Docker Desktop, los montajes vinculados utilizan gRPC FUSE, que es significativamente más lento que en Linux. Para los servidores de desarrollo NestJS y Next.js, ejecútelos directamente en su máquina host (no en Docker) para obtener el rendimiento del sistema de archivos nativo.
Producción Docker Compose
El archivo de redacción de producción es estructuralmente diferente: sin puertos locales, políticas de reinicio, límites de recursos de producción:
# infrastructure/docker-compose.prod.yml
name: ecosire-prod
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
# No port mapping — only accessible within Docker network
restart: always
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 512M
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 1gb
volumes:
- redis_data:/data
restart: always
# No port mapping — internal only
volumes:
postgres_data:
redis_data:
La producción no expone los puertos externamente: las aplicaciones se conectan a través de la red interna de Docker. Nginx maneja el tráfico externo.
Errores y soluciones comunes
Error 1: el puerto entra en conflicto con los servicios del sistema
PostgreSQL, Redis y otros servicios suelen ejecutarse como servicios del sistema. Asigne siempre a puertos no estándar en desarrollo:
- PostgreSQL:
5433:5432(no5432:5432) - Redis: mantener
6379:6379(rara vez entra en conflicto) - Ejecute
lsof -i :5432para verificar qué está usando el puerto predeterminado
Error 2: problemas de permisos de volumen en Linux
Los volúmenes Docker en Linux utilizan la propiedad raíz de forma predeterminada. Si el usuario de su contenedor no es root, establezca la propiedad correcta:
postgres:
image: postgres:17-alpine
user: "999:999" # postgres user's UID:GID
# Or use init container to fix permissions
Error 3: la inicialización de Authentik tarda más de 60 segundos
Authentik ejecuta migraciones de bases de datos en el primer inicio. El start_period: 60s en el control de salud le da tiempo. Si los servicios dependientes se inician antes de que Authentik esté listo, fallarán. Utilice la condición service_healthy y déle suficiente start_period.
Error 4: Límites de recursos de Docker Desktop en Mac
Docker Desktop predeterminado asigna 2 CPU y 2 GB de RAM, lo que no es suficiente para que PostgreSQL + Redis + Authentik se ejecuten simultáneamente. Aumento de Configuración de escritorio Docker > Recursos a al menos 4 CPU y 6 GB de RAM.
Error 5: docker-compose frente a docker compose
El antiguo docker-compose (v1, escrito en Python) está obsoleto. Utilice docker compose (v2, el complemento). Verifique su versión: docker compose version. Si ves Docker Compose version v2.x.x, estás usando v2.
Preguntas frecuentes
¿Debo ejecutar los servicios de mi aplicación (NestJS, Next.js) en Docker durante el desarrollo?
Generalmente no: para un desarrollo activo, ejecute los servicios de su aplicación en su máquina host para una recarga en caliente más rápida y una depuración más sencilla. Utilice Docker solo para servicios de infraestructura (bases de datos, cachés, proveedores de identidad) que sean estables y no necesiten reinicios frecuentes. La excepción es si su aplicación tiene dependencias nativas que difieren entre su sistema operativo de desarrollo y el entorno de producción.
¿Cómo manejo las migraciones de bases de datos en el flujo de trabajo de Docker Compose?
Ejecute migraciones desde su máquina host después de iniciar la infraestructura: pnpm dev:infra && pnpm db:migrate. No ejecute migraciones dentro de un contenedor Docker durante el desarrollo; perderá la verificación de tipos y la integración IDE que hace que las migraciones de Drizzle sean seguras. Para la creación inicial de la base de datos, utilice los scripts initdb.d de Docker.
¿Cómo hago una copia de seguridad y restauro mis volúmenes Docker locales?
Utilice docker run --rm -v postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres-backup.tar.gz /data para realizar una copia de seguridad. Restaure con el mismo enfoque usando tar xzf. Para desarrollo, también puedes volcar con pg_dump y restaurar con psql ya que tienes el puerto expuesto.
¿Cómo comparto el estado de Docker Compose con otros miembros del equipo?
El archivo Docker Compose se comparte a través de git, pero los datos de los volúmenes son locales. Cada desarrollador comienza con una base de datos vacía y ejecuta migraciones/semillas para poblarla. Utilice secuencias de comandos semilla (confirmadas en el repositorio) para crear datos de prueba consistentes. El docker-compose.dev.yml compartido garantiza que todos utilicen las mismas versiones y configuración del servicio.
¿Por qué utilizar Mailpit en lugar del correo electrónico real en el desarrollo?
Mailpit es un servidor SMTP local que captura todos los correos electrónicos salientes y proporciona una interfaz de usuario web para verlos. Evita el envío accidental de correos electrónicos reales a usuarios reales durante el desarrollo, no requiere credenciales SMTP y le permite verificar plantillas de correo electrónico sin revisar su bandeja de entrada. Configure su aplicación para usar SMTP_HOST=localhost SMTP_PORT=1025 y visite http://localhost:8025 para ver los correos electrónicos capturados.
Próximos pasos
Una configuración de Docker Compose bien diseñada para el desarrollo local es una inversión que rinde dividendos cada vez que se une un nuevo desarrollador o se pone en marcha una nueva máquina. ECOSIRE ejecuta PostgreSQL 17, Redis 7 y Authentik en Docker Compose para el desarrollo local en todo el equipo.
¿Necesita ayuda para diseñar su infraestructura de desarrollo local o contener su aplicación para producción? Explore nuestros servicios DevOps para ver cómo podemos ayudarle.
Escrito por
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
Artículos relacionados
Guía de implementación de AWS EC2 para aplicaciones web
Guía completa de implementación de AWS EC2: selección de instancias, grupos de seguridad, implementación de Node.js, proxy inverso de Nginx, SSL, escalado automático, monitoreo de CloudWatch y optimización de costos.
ERP en la nube versus local en 2026: la guía definitiva
ERP en la nube versus local en 2026: análisis de costos totales, comparación de seguridad, escalabilidad, cumplimiento y el modelo de implementación adecuado para su negocio.
Creación de módulos Odoo personalizados: tutorial para desarrolladores
Tutorial paso a paso para crear módulos personalizados de Odoo 19. Cubre la estructura del módulo, modelos, vistas, seguridad, asistentes y mejores prácticas para código listo para producción.