Every project has a setup document. "Install Postgres 15. Create a database called app_dev. Install Redis. Set the REDIS_URL environment variable. Install Node 20. Run npm install. Start the server." Docker Compose replaces all of that with a single file that anyone can run with one command.
I converted my team's 4-page setup document into a docker-compose.yml file that takes 30 seconds to start. Here is what I learned about getting it right.
The structure of a compose file
A docker-compose.yml defines services (containers), their configuration, and how they connect:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/app
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=app
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:
docker compose up starts all three services, creates the network, sets up the volume, and your app is running with a real database and cache.
Key configuration patterns
Networking: By default, all services in a compose file share a network. Service names resolve as hostnames. The app container can reach Postgres at db:5432 and Redis at cache:6379. No IP addresses, no manual network configuration.
Volumes: Named volumes persist data between container restarts. Without the pgdata volume, your database would be wiped every time you recreate containers. Bind mounts (./src:/app/src) map host directories into containers for development with live reload.
Build vs Image: build: . builds from a local Dockerfile. image: postgres:15 pulls a pre-built image from Docker Hub. Your application services typically use build, while databases, caches, and other infrastructure use image.
Environment variables: Can be set inline, loaded from a file (env_file: .env), or reference host environment variables. For sensitive values, use .env files that are in .gitignore.
Depends_on: Controls startup order but does not wait for the service to be "ready." Postgres might start before it is accepting connections. For true readiness checks, use healthcheck with depends_on condition:
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
app:
depends_on:
db:
condition: service_healthy
Development vs production patterns
Development compose files should include:
- Volume mounts for live code reload
- Debug ports exposed
- Development-mode environment variables
- Hot reload configurations
Production compose files should include:
- Resource limits (CPU, memory)
- Restart policies (
restart: unless-stopped) - No volume mounts for source code (baked into the image)
- Health checks
- Logging configuration
Use multiple compose files with docker compose -f docker-compose.yml -f docker-compose.dev.yml up to override production settings with development ones.
Common service templates
Postgres + pgAdmin:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: devpass
volumes:
- pgdata:/var/lib/postgresql/data
pgadmin:
image: dpage/pgadmin4
ports:
- "5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@dev.local
PGADMIN_DEFAULT_PASSWORD: admin
Node.js with hot reload:
app:
build: .
command: npm run dev
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
The /app/node_modules anonymous volume prevents the host's node_modules from overriding the container's.
The generator
Building compose files from scratch means remembering service-specific environment variables, port mappings, volume paths, and configuration options for every service. I built a Docker Compose generator that lets you select your services, configure the options, and generates a ready-to-use compose file.
I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.
Top comments (0)