Saltar al contenido

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.

4 min de lectura
Terminal mostrando el inicio de servicios de Docker Compose con logs para cada contenedor

"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.

ymlyaml
# 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 — pgdata persiste los datos entre reinicios. Sin él, pierdes la base de datos cada vez que ejecutas docker compose down.
  • Health checks en las dependencias — depends_on con condition: service_healthy evita 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.

dockerfiledockerfile
# 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"]
dockerfiledockerfile
# ❌ 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.

jsjavascript
// 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):

jsonjson
{
  "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.

ymlyaml
# 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 conflicts

Para pruebas parecidas a producción, usa un archivo de sobrescritura explícito:

ymlyaml
# docker-compose.prod.yml
services:
  app:
    build:
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=production
    volumes: []  # No source mount — use built image
shbash
# Development (default)
docker compose up
 
# Production-like testing
docker compose -f docker-compose.yml -f docker-compose.prod.yml up

Gestión de la base de datos

Las bases de datos de desarrollo necesitan seeding, migraciones y resets ocasionales. Automatiza esto con comandos de compose.

ymlyaml
# 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
shbash
# 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 seed

La 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.

ymlyaml
# docker-compose.yml
services:
  app:
    command: node --inspect=0.0.0.0:9229 node_modules/.bin/next dev
    ports:
      - "3000:3000"
      - "9229:9229"
jsonjson
// .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

ymlyaml
# ❌ 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}
shbash
# .env (add to .gitignore)
DB_PASSWORD=local-dev-only-password

Otros 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:latest hoy no es postgres:latest mañana. Fija las versiones.

Conclusiones clave

  1. Replica la infraestructura de producción — usa la misma versión de base de datos, mismos servicios, misma configuración
  2. Separa la instalación de dependencias del código fuente — el orden de capas del Dockerfile mantiene las reconstrucciones rápidas
  3. Monta el código fuente como volumen para la recarga en caliente — no copies archivos fuente en desarrollo
  4. Usa health checks en las dependencias — depends_on sin condiciones no espera a que estén listas
  5. Usa profiles para servicios de utilidad — las migraciones y seeds no deben arrancar automáticamente
  6. Fija las versiones de las imágenes — postgres:16-alpine, no postgres:latest
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX