Docker Compose Multi-Container Deployment: Building Complete Application Orchestration
A typical web application is rarely a single process: the frontend needs a container, the backend API another, the database another, the cache another — and often a background job queue on top. Starting each of those with docker run locally means keeping ports, networks, and dependency order in your head; hand the project to a colleague and they have to reconstruct the whole setup from scratch. Docker Compose solves exactly this pain. If you are not yet familiar with Docker itself, start with the Docker deployment guide; here we use Compose directly to define the entire stack — one YAML file lays out the services, one command brings everything up, and the whole team shares the same startup procedure.
1. Basic Usage
Put a docker-compose.yml in the project root declaring three services: the web app, a PostgreSQL database, and a Redis cache:
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
depends_on:
- db
- redis
db:
image: postgres:16
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: secret
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres-data:
A few points worth noting: build: . means the web service is built from the Dockerfile in the current directory; depends_on declares startup order — start db and redis before web. But it only guarantees "started," not "ready" — waiting until the database actually accepts connections requires the health checks below. postgres-data is a named volume; the database data lives there, so deleting the container does not delete the data.
2. Network Configuration
Compose creates a network for the project by default, and services reach each other by service name. Inside the web container you connect to the database with db:5432, not localhost:5432. This also isolates projects: two projects can both have a service named web without conflict, because their networks are separate.
For finer control, define custom networks and isolate services by layer:
services:
frontend:
networks:
- frontend
- backend
api:
networks:
- backend
db:
networks:
- backend
networks:
frontend:
backend:
In this configuration frontend can reach both the frontend and backend networks, while api and db live only in backend. Even if one service is compromised, the network surface it can reach stays limited.
Compose also lets you put the "number of services" into the config: define deploy.replicas: 2 on the web service, and docker compose up --scale web=3 temporarily scales it to 3 replicas. Compose scaling is not production-grade load balancing, but it is plenty for local load testing and demo environments — which is also why many teams use Compose for "fake production" rehearsals.
3. Environment Management and Multiple Compose Files
Development and production environment variables differ a lot: debug switches, log levels, and port mappings. Compose supports splitting files by environment and layering them with -f:
# docker-compose.override.yml (Development)
services:
web:
environment:
- DEBUG=true
volumes:
- .:/app
# docker-compose.prod.yml (Production)
services:
web:
environment:
- NODE_ENV=production
- DEBUG=false
# Development
docker compose up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
In development the current directory is mounted into the container so code changes take effect immediately; in production the built image is used with debugging off. Manage sensitive configuration in a .env file — Compose reads a .env in the same directory automatically and injects the variables into containers.
4. Health Checks and Startup Order
depends_on handles ordering, not readiness — it cannot stop web from failing to connect before the database is ready. Combining it with health checks makes Compose truly wait:
services:
web:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
A more robust pattern is adding condition: service_healthy to web's depends_on, so Compose waits for the database's health check to pass before starting web. Database images usually ship with pg_isready, which works well as a health check command.
5. Common Commands
| Command | Purpose |
|---|---|
docker compose up -d |
Start in background |
docker compose down |
Stop and remove containers and networks |
docker compose logs -f |
Follow logs in real time |
docker compose build |
Build images |
docker compose exec web sh |
Run a command inside a container |
docker compose restart web |
Restart a single service |
down does not remove named volumes, so database data survives by default. Use docker compose down -v only when you truly want to wipe data — confirm there is nothing to keep first.
6. Production Best Practices
- Manage environment variables in a
.envfile; inject sensitive values from a secret manager. - Configure resource limits (CPU/memory) so a single container cannot exhaust the whole host.
- Set
restart: unless-stoppedso services come back automatically after a server reboot. - Persist data in named volumes; never store state in a container's writable layer.
- Never hardcode secrets into images or Compose files — inject them via secrets or environment variables.
A Complete Example
An e-commerce admin backend consists of a Laravel app, MySQL, Redis, and a queue worker. Defined as four Compose services, docker compose up brings the whole stack up locally with one command — the closest thing to one-click development. In production, an overlay prod file is used, MySQL data lives in a named volume, and the worker shares the same image as the web app. A new teammate clones the repo, runs docker compose up, and starts developing — eliminating the "can't get the environment running" problem that blocks most onboarding. This approach has a bonus too: because dependencies, ports, and volumes are all written in the Compose file, code review shows the environment definition directly, and adding a new service (say Elasticsearch) is a few lines of YAML that gives everyone the same environment.
Common Questions
- Services cannot reach each other? Check that you are using service names instead of
localhost, and that both services are on the same network. - Port conflict? Change the host-side port on the left of the
portsmapping; the container-side port stays unchanged. - Data gone after restart? Check whether the data was written to the container's writable layer instead of a named volume — the writable layer disappears when the container is deleted.
- Database unreachable, service crashing instantly? Most likely the database was not ready when web started. Add a healthcheck to db and configure
condition: service_healthyin web'sdepends_on.
Reference: Docker Compose documentation https://docs.docker.com/compose/ ; Compose Specification https://compose-spec.io/