How to use Docker Compose: multi-container apps explained
Learn Docker Compose to run multi-container apps from one YAML file: services, networks, volumes, health checks, Compose Watch, env vars, docker compose up and common commands — with a real Node.js example.
On this page
- What Docker Compose is
- The Compose file
- Starting the stack
- Viewing status and logs
- Running commands inside a running container
- Environment variables with .env
- Health checks and startup order
- Persisting data with named volumes
- Compose Watch for live reload
- Inspecting config before it runs
- Tearing down the stack
- FAQ
- What is the difference between Docker and Docker Compose?
- Do I need a separate install for Docker Compose?
- What does docker compose up do?
- How do I keep data when I restart a container?
A single Docker container runs one process. Real applications usually need several: a web server, a database, a cache, a queue worker. Docker Compose lets you define and run all these services together from one YAML file, then start the whole stack with a single command. This guide shows how Compose works and how to use it with a practical Node.js example: a web app plus a Redis cache, with health checks, persistent volumes, environment variables, and the everyday docker compose commands you will actually run.
What Docker Compose is
Docker Compose is a tool for defining and running multi-container Docker applications. It controls your whole stack — services, networks, and volumes — in a single YAML configuration file. With one command you create and start all the services from that file, instead of running many long docker run commands with dozens of flags.
Compose lets you:
- Start, stop, and rebuild services
- View the status of running services
- Stream the log output of running services
- Run a one-off command on a service
Compose is included with Docker Desktop (Windows and macOS) and with the Docker Engine on Linux, so after installing Docker you usually already have it. Verify with docker compose version.
The Compose file
The default file is compose.yaml in your project directory (older docker-compose.yml is also accepted). It is written in YAML, and its most important top-level elements are services, volumes, and networks:
- services — the containers to run, each built from an image or a Dockerfile.
- volumes — named storage that persists data between container restarts.
- networks — how services communicate (Compose creates a default network for you).
A minimal two-service Compose file for a Node.js app plus a Redis cache looks like this:
services:
web:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
depends_on:
redis:
condition: service_healthy
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
Here the web service is built from the Dockerfile in the current directory (note build: .), while redis uses the public redis:7-alpine image pulled from Docker Hub. Every Compose project gets a default network, so web can reach redis by its service name.
Starting the stack
The command that creates and starts every service from your file is:
$ docker compose up
This builds the images that need building, pulls any remote images, and starts all containers. Running it in the foreground shows streaming logs from every service. To run in the background and free your terminal, add the detached flag:
$ docker compose up -d
Viewing status and logs
Check the state of all services:
$ docker compose ps
Follow log output from all services, color-coded by service name:
$ docker compose logs -f
To follow logs for one service only, pass its name:
$ docker compose logs -f web
Running commands inside a running container
docker compose exec runs a command inside an already-running container — the primary tool for live debugging. For example, to check that your Node.js app can reach Redis, resolve the service name:
$ docker compose exec web node -e "const r=require('redis');console.log('ok')"
To confirm environment variables propagated correctly:
$ docker compose exec web env | grep REDIS
Environment variables with .env
Keep configuration out of the Compose file where possible. Compose automatically reads a .env file in the project directory and makes its values available for interpolation in your YAML. For example, with .env containing:
APP_PORT=3000
REDIS_HOST=redis
REDIS_PORT=6379
You can reference these in compose.yaml as ${APP_PORT}, ${REDIS_HOST}, and ${REDIS_PORT}. Keeping values in .env makes it easier to change configuration across environments without editing YAML.
Health checks and startup order
Containers start in parallel, so a web app often tries to connect to a database before it is ready. A health check tells Compose when a service is truly ready, and depends_on with a condition makes one service wait for another.
The healthcheck block on redis runs redis-cli ping and expects a PONG reply. The start_period of 10 seconds lets Redis initialize before checks begin, and interval/timeout/retries control how often the check runs and how many failures are tolerated.
depends_on with condition: service_healthy makes web wait until redis is confirmed healthy before starting. This fixes the classic startup race where the app connects before the cache is up.
Persisting data with named volumes
Container filesystems are ephemeral — when you remove a container, everything written inside is lost. A named volume keeps data on the host and reattaches it on the next up. Attach a volume with volume-name:/container/path:
services:
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
redis-data:
The top-level volumes key registers redis-data so it persists across docker compose down and up cycles. This is how you keep a database or cache between restarts.
Compose Watch for live reload
Compose's develop.watch block gives live updates during development. Running docker compose up --watch, Compose watches files and applies rules you define:
services:
web:
build: .
develop:
watch:
- action: sync+restart
path: .
target: /app
sync+restartcopies changed files into the running container, then restarts it.rebuildtriggers a full image rebuild — useful when you add a dependency (for example a newpackage.jsonentry) that requires reinstalling packages.
Inspecting config before it runs
docker compose config shows the fully resolved configuration without starting anything. It works purely from your files and is invaluable for debugging variable substitution and file merging — it shows environment variables replaced, port notation expanded into canonical fields, and default network and volume names made explicit.
Tearing down the stack
Stop and remove the containers (named volumes are kept):
$ docker compose down
Remove containers and named volumes too (this permanently deletes stored data — use intentionally):
$ docker compose down -v
FAQ
What is the difference between Docker and Docker Compose?
Docker builds and runs individual containers; Docker Compose orchestrates multiple containers as one application defined in a single YAML file. You use Docker to build and run one image, and Compose to define and run a whole stack of services, networks, and volumes together.
Do I need a separate install for Docker Compose?
No. Docker Compose is included with Docker Desktop on Windows and macOS and with the Docker Engine on Linux. After installing Docker, verify it with docker compose version.
What does docker compose up do?
It reads your compose.yaml, builds any services with a build: directive, pulls any remote images, and starts all containers. Adding -d runs the stack in detached (background) mode; --watch enables live reload during development.
How do I keep data when I restart a container?
Use a named volume. Declare it in the top-level volumes key and mount it at the data path (for example redis-data:/data). Named volumes persist across docker compose down and up. To delete stored data as well, run docker compose down -v.
Sources
- Docker Docs — Docker Compose (docs.docker.com/compose)
- Docker Docs — Compose Quickstart (docs.docker.com/compose/gettingstarted)
- Docker Docs — Compose file reference (docs.docker.com/reference/compose-file)
- Docker Docs — Dockerfile reference (docs.docker.com/reference/dockerfile)
- Docker — Components and licenses (docker.com/legal/components-licenses
- Apache-2.0)
Share this guide
Related guides
- How to containerize a Node.js app with Docker
Containerize a Node.js app with Docker: write a Dockerfile, use base images and multi-stage builds, expose ports, manage dependencies and environment variables, and run your container locally — open-license sources.
- Docker explained: how containers work
Docker explained: what containers are, how Docker Engine uses Linux namespaces and cgroups, how images, Dockerfiles, and Docker Compose fit together, and how Docker compares with virtual machines.
- How to set up CI/CD with GitHub Actions
Set up a CI/CD pipeline with GitHub Actions: create your first workflow, configure triggers, build and test your code on runners, use actions and secrets, and deploy automatically — open-license sources.