Docker Compose is a small but powerful tool: it lets you describe a multi-container application in one file and bring it up with a single command. If you’ve ever juggled a Node process, a database, and a cache across multiple terminals and env files, Compose will save you time and prevent a lot of errors.
Why Compose (and when to use it)
Compose is ideal for local development, CI test environments, and simple staging stacks. It is not a replacement for Kubernetes for large-scale production deployments, but it frequently sits in the sweet spot for developer productivity:
- One declarative file (docker-compose.yml / compose.yaml).
- Consistent environment across machines and CI.
- Easy lifecycle commands: up, down, logs, exec.
Use Compose when you need to run multiple dependent services (app + DB + cache + worker) locally or for small-scale testing.
Basic example
This minimal example shows a Node app, Postgres, and Redis. Save as docker-compose.yml next to your Dockerfile.
version: "3.8"
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- ./:/usr/src/app:cached
- /usr/src/app/node_modules
environment:
NODE_ENV: development
depends_on:
- db
- redis
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
db:
image: postgres:14
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: appdb
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
redis:
image: redis:7
volumes:
- redis-data:/data
volumes:
db-data: {}
redis-data: {}
Notes:
- Use depends_on for start order, but prefer healthcheck + wait-for logic in the app for robust readiness.
- Bind-mount the source for fast iteration; exclude node_modules by mounting a named anonymous path to avoid host/containing conflicts.
Common commands
# Start in foreground (good for debugging)
docker compose up
# Start detached
docker compose up -d
# Rebuild a service and restart
docker compose up -d --build app
# See status
docker compose ps
# Tail logs (all services)
docker compose logs -f
# Run a command in a running container
docker compose exec app sh
# Stop and remove containers, networks created by compose
docker compose down --volumes --remove-orphans
Tip: modern Docker supports both docker-compose (hyphen) and docker compose (space). Prefer the built-in “docker compose” where available.
Best practices and patterns
Keep config out of the YAML
Use env_file and .env to store runtime variables; avoid hardcoding secrets. For real secrets use Docker secrets or a secrets manager.
Use healthchecks for orchestration
depends_on only waits for container start, not readiness. Add healthcheck blocks and make your app retry DB connections.
Split dev and prod settings
Use override files (docker-compose.override.yml) or profiles so dev mounts the code while production uses built images.
Avoid permission and node_modules issues
For Node apps with bind mounts, map a host folder for node_modules or use an anonymous volume to let the container manage dependencies.
Keep volumes for persistent services
Use named volumes for DB data so containers can be replaced without losing state.
Limit exposed ports in CI
In CI, prefer internal networks and avoid mapping ports unless necessary. Compose creates a network per project by default.
Dev tricks that save time
- docker compose up –build –force-recreate to rebuild and recreate containers.
- Use profiles to enable heavy services only when needed (e.g., monitoring):
services:
debug-tool:
image: some/debug
profiles: [dev-tools]
Start with a profile: docker compose –profile dev-tools up
- Use depends_on + healthcheck for basic orchestration, but for robust startup use wait scripts or retry logic in your app.
Troubleshooting
Container exits immediately
Run docker compose logs
Port conflicts
Check running containers with docker ps and free the host port or change mapping in compose.
Database not ready
Even with depends_on, your app may attempt DB connections before the DB is fully ready. Add a retry/backoff in the app or use a small wait-for script.
Volume caching and slow mounts on macOS
Bind mounts can be slow on macOS/Windows. Use cached or delegated options (as in the example) or use Docker Desktop file sharing settings.
When Compose is not enough
Compose is not a drop-in production orchestrator for large clusters. For high-availability, scaling across hosts, or advanced scheduling, move to Kubernetes, ECS, or another orchestrator. However, Compose files can often be used as a reference when authoring Helm charts or Kubernetes manifests.
Example workflow: iterate fast
- docker compose up -d db redis
- docker compose up –build app (or use docker compose up -d –build)
- docker compose logs -f app
- When changing a Dockerfile, run docker compose up -d –build app
- When testing a clean DB: docker compose down –volumes && docker compose up -d
Conclusion
Docker Compose gives you a predictable, repeatable way to run multiple services locally. Use healthchecks, env_files, and volumes properly; split dev and prod settings; and prefer small, focused compose files. With these conventions you’ll spend less time juggling terminals and more time shipping features.
If you want, I can produce a starter repo template (Dockerfile + compose + healthcheck-ready app) tailored to Node, Python, or another stack—tell me which stack you prefer.
