Saltar al contenido principal

Docker Compose para el desarrollo: infraestructura local

Docker Compose para desarrollo local: PostgreSQL, Redis, Authentik, redes, controles de estado, administración de volúmenes y configuraciones específicas del entorno para monorepositorios de TypeScript.

E
ECOSIRE Research and Development Team
|19 de marzo de 202611 min de lectura2.1k Palabras|

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.

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_file para cargar variables de entorno desde su archivo .env.local en contenedores
  • Separe docker-compose.dev.yml de docker-compose.prod.yml: tienen diferentes propósitos
  • El patrón depends_on.condition: service_healthy espera la preparación real, no solo el inicio del contenedor
  • Utilice profiles para optar por servicios opcionales (correo electrónico, monitoreo)
  • Ejecute docker compose (v2), no docker-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: admin@local.dev
      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:

TipoPersistenciaRendimientoUsar para
Volumen nombradoSíExcelenteDatos de la base de datos
Montaje de enlaceSíBueno (Linux), Malo (macOS)Recarga en caliente del código fuente
tmpfsNoExcelenteArchivos 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 (no 5432:5432)
  • Redis: mantener 6379:6379 (rara vez entra en conflicto)
  • Ejecute lsof -i :5432 para 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.

E

Escrito por

ECOSIRE Research and Development Team

Redacción técnica

El equipo de redacción técnica de ECOSIRE escribe sobre Odoo ERP, comercio electrónico con Shopify, agentes de IA, analítica con Power BI, automatización con GoHighLevel y buenas prácticas de software empresarial. Nuestras guías ayudan a las empresas a tomar decisiones tecnológicas informadas.

ECOSIRE

Haga crecer su negocio con ECOSIRE

Soluciones empresariales en ERP, comercio electrónico, inteligencia artificial, análisis y automatización.