Docker Compose: Multi-Service Local Development
Dominá Docker Compose para desarrollo local: entornos multi-service, networking, volumes, profiles, overrides, hot reload, debugging y setups production-like.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Introducción
Docker Compose define y runnea multi-container applications localmente. Describís services, networks y volumes en un YAML file, luego starteás todo con un command. A continuación: service definitions, networking, volumes, profiles, override files, hot reload setups, debugging y production-like local environments.
Basic docker-compose.yml
# docker-compose.yml
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
- REDIS_URL=redis://redis:6379
volumes:
- .:/app
- /app/node_modules # Anonymous volume prevents host override
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
volumes:
db-data:
redis-data:
Networking
# Default network: all services join un default network named after el project
# Services se reachéan entre sí by service name as hostname
services:
app:
# app reachéa db at: postgresql://db:5432
# app reachéa redis at: redis://redis:6379
networks:
- default
db:
networks:
- default
# Custom networks para isolation
frontend:
image: nginx:alpine
networks:
- frontend-net
backend:
image: my-backend
networks:
- frontend-net # Puede reach frontend
- backend-net # Puede reach db
db:
image: postgres:16
networks:
- backend-net # Solo backend puede reach db
networks:
frontend-net:
driver: bridge
backend-net:
driver: bridge
internal: true # No external access (isolated)
Volumes
services:
app:
volumes:
# Bind mount: host directory → container (hot reload)
- .:/app
- ./config:/app/config:ro # Read-only mount
# Anonymous volume: prevente host override de specific path
- /app/node_modules
# Named volume: persistent data managed by Docker
- app-cache:/app/.cache
db:
volumes:
- db-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
# init-scripts runnean en first start only (cuando db-data está empty)
volumes:
db-data:
driver: local
app-cache:
Override Files
# docker-compose.yml — Base configuration (shared across environments)
version: "3.9"
services:
app:
build: .
environment:
- NODE_ENV=development
ports:
- "3000:3000"
# docker-compose.override.yml — Local dev overrides (auto-loaded)
# Docker Compose automáticamente mergea: docker-compose.yml + docker-compose.override.yml
version: "3.9"
services:
app:
volumes:
- .:/app
- /app/node_modules
command: npm run dev
environment:
- NODE_ENV=development
- DEBUG=app:*
ports:
- "3000:3000"
- "9229:9229" # Node.js debugger
# docker-compose.prod.yml — Production-like config (explicit -f flag)
# Usage: docker compose -f docker-compose.yml -f docker-compose.prod.yml up
version: "3.9"
services:
app:
volumes: [] # Remové bind mounts
command: node server.js
environment:
- NODE_ENV=production
ports:
- "80:3000"
restart: always
deploy:
replicas: 3
resources:
limits:
cpus: '1.0'
memory: 512M
Profiles
# Profiles te dejan startéar specific groups de services
# docker compose --profile debug up
# docker compose --profile monitoring up
version: "3.9"
services:
app:
# No profile — siempre startea
build: .
db:
# No profile — siempre startea
image: postgres:16
mailhog:
# Solo startea con --profile debug
image: mailhog/mailhog
profiles: [debug]
ports:
- "1025:1025"
- "8025:8025"
prometheus:
image: prom/prometheus
profiles: [monitoring]
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
profiles: [monitoring]
ports:
- "3001:3000"
Hot Reload Setup
Node.js con nodemon
services:
app:
build: .
volumes:
- .:/app
- /app/node_modules
command: npx nodemon --watch src --ext ts,json --exec "node --import tsx src/index.ts"
environment:
- NODE_ENV=development
ports:
- "3000:3000"
Python con uvicorn auto-reload
services:
api:
build: .
volumes:
- .:/app
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload --reload-dir /app/src
environment:
- PYTHONUNBUFFERED=1
ports:
- "8000:8000"
Java con Spring Boot DevTools
services:
app:
build: .
volumes:
- ./target:/app/target
command: java -jar -Dspring.devtools.restart.enabled=true target/app.jar
environment:
- SPRING_DEVTOOLS_RESTART_TRIGGER_FILE=/app/target/.trigger
ports:
- "8080:8080"
Multi-Service Full Stack Application
version: "3.9"
services:
frontend:
build: ./frontend
volumes:
- ./frontend:/app
- /app/node_modules
command: npm run dev -- --host 0.0.0.0
ports:
- "5173:5173"
environment:
- VITE_API_URL=http://localhost:3000/api
depends_on:
- backend
backend:
build: ./backend
volumes:
- ./backend:/app
- /app/node_modules
command: npm run dev
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
- REDIS_URL=redis://redis:6379
- JWT_SECRET=dev-secret
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
worker:
build: ./backend
volumes:
- ./backend:/app
- /app/node_modules
command: npm run worker
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
- ./db-init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
depends_on:
- frontend
- backend
volumes:
db-data:
redis-data:
Debugging
Node.js debugging
services:
app:
command: node --inspect=0.0.0.0:9229 src/index.js
ports:
- "3000:3000"
- "9229:9229" # Debugger port
Python debugging con debugpy
services:
api:
command: python -m debugpy --listen 0.0.0.0:5678 --wait-for-client src/main.py
ports:
- "8000:8000"
- "5678:5678"
Java remote debugging
services:
app:
command: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar
ports:
- "8080:8080"
- "5005:5005"
Useful Commands
# Starteá all services (foreground)
docker compose up
# Starteá en background (detached)
docker compose up -d
# Starteá specific services
docker compose up app db
# Starteá con profiles
docker compose --profile monitoring up
# Rebuildéa y starteá
docker compose up --build
# Viewéa logs
docker compose logs -f
docker compose logs -f app
docker compose logs --tail=100 app
# Stopéa y remové containers
docker compose down
# Stopéa y remové containers + volumes
docker compose down -v
# Stopéa y remové containers + volumes + images
docker compose down -v --rmi all
# Runnéa un one-off command
docker compose exec app npm run migrate
docker compose run --rm app npm run test
# Restarteá un single service
docker compose restart app
# Viewéa running services
docker compose ps
# Viewéa resource usage
docker compose stats
Best Practices
-
For a deeper guide, see Complete Guide to Docker in Production.
-
Usá bind mounts para source code en dev — habilita hot reload sin rebuilds
-
Usá anonymous volumes para
node_modules,.venv— prevente host OS de overriding container packages -
Usá
depends_onconcondition: service_healthy— ensureá que DB esté ready antes de que app start -
Usá override files para environment-specific config — base file + override auto-merged
-
Usá profiles para optional services — monitoring, mail catcher, debug tools
-
Seteá
restart: unless-stoppedpara dev databases — survive machine reboots -
Usá
.dockerignore— preventí copyingnode_modules,.git, build artifacts into images -
Pinneá image versions —
postgres:16-alpinenopostgres:latest -
Usá healthchecks en databases — app waitéa readiness en vez de failing on connection
-
Usá named volumes para persistent data — survive
docker compose down(usá-vpara remove) -
Mantené
docker-compose.ymlen version control — reproducible environments para todo el team
Common Mistakes
- No anonymous volume para
node_modules: bind mount overwritea container-installed packages con host’s (o empty) directory. - Usar
depends_onsin healthcheck: app startea antes de que DB esté ready, connection fails on boot. - No
.dockerignore: build context incluyenode_modules,.git, large files — slow builds y bloated images. - Usar
latesttag: images cambian unexpectedly entre pulls. Pinneá a specific versions. - No volume para database data:
docker compose downdestroyéa all data. Usá named volumes. - Exponer all ports a host: solo exponé ports que necesitás accessar desde el host. Internal services communicatean via Docker network.
FAQ
¿Cuál es la diferencia entre bind mounts y named volumes?
Bind mounts mapean un host directory a un container path. Habilitan hot reload pero tiean el container al host filesystem. Named volumes son managed by Docker y persisten independientemente del container. Usá bind mounts para source code, named volumes para database data.
¿Cómo funciona docker-compose.override.yml?
Docker Compose automáticamente mergea docker-compose.yml con docker-compose.override.yml si existe. El override file agrega o reemplaza settings del base file. Usalo para local dev settings. Para production, usá explicit -f flags: docker compose -f docker-compose.yml -f docker-compose.prod.yml.
¿Qué son Docker Compose profiles?
Labels que groupéan services. Services sin profile siempre startean. Services con profile solo startean cuando pasás --profile <name>. Useful para optional services como monitoring, debug tools, o mail catchers.
¿Cómo runneo database migrations?
Usá docker compose exec para runnear un one-off command en un running container: docker compose exec app npm run migrate. O usá docker compose run para un new container: docker compose run --rm app npm run migrate.
¿Cómo reseteo mi local environment?
docker compose down -v stopea y removeéa containers y volumes. Esto wipeéa all data. Para keep data, usá docker compose down sin -v. Para rebuild from scratch: docker compose build --no-cache && docker compose up.
Recursos Relacionados
GitHub Actions CI/CD: Workflows, Runners, Secrets
Master GitHub Actions for CI/CD: workflows, reusable workflows, composite actions, secrets management, runners, matrix builds, caching, and deployment patterns.
GuideHelm Charts: Structure, Templating, Dependencies, Registry
Master Helm charts for Kubernetes: chart structure, templating, values, dependencies, hooks, libraries, registry management, and production patterns for deployment.
GuideComplete Guide to Docker in Production
Run Docker containers in production with confidence. Covers multi-stage builds, distroless images, health checks, image scanning, resource limits, logging, secrets, multi-arch builds, and container runtime security with practical Dockerfile examples.
GuideDocker for Developers — A Complete Guide
Learn Docker from the ground up: images, containers, Dockerfiles, networks, volumes, and Docker Compose for local development.
GuideKubernetes Basics for Application Developers
Learn the core Kubernetes concepts every developer needs: Pods, Services, Deployments, ConfigMaps, and basic kubectl commands.