Docker Compose: Define and Run Multi-Container Apps
Docker Compose is a tool for defining and running multi-container applications. Instead of starting each container separately with docker run , you describe all of your services, networks, and volumes in a single YAML file and bring the entire environment up with one command.
This guide explains how Docker Compose V2 works, walks through a practical example with a SvelteKit development server and PostgreSQL database, and covers the most commonly used Compose directives and commands.
Quick Reference
For a printable quick reference, see the Docker cheatsheet .
| Command | Description |
|---|---|
docker compose up |
Start all services (foreground) |
docker compose up -d |
Start all services in detached mode |
docker compose down |
Stop and remove containers and networks |
docker compose down -v |
Also remove named volumes |
docker compose ps |
List running services |
docker compose logs SERVICE |
View logs for a service |
docker compose logs -f SERVICE |
Follow logs in real time |
docker compose exec SERVICE sh |
Open a shell in a running container |
docker compose stop |
Stop containers without removing them |
docker compose build |
Build or rebuild images |
docker compose pull |
Pull the latest images |
Prerequisites
The examples in this guide require Docker
with the Compose plugin installed. Docker Desktop includes Compose by default. On Linux, install the docker-compose-plugin package alongside Docker Engine.
To verify Compose is available, run:
docker compose versionDocker Compose version v2.x.x
Compose V2 runs as docker compose (with a space), not docker-compose. The old V1 binary is end-of-life and not covered in this guide.
The Compose File
Modern Docker Compose prefers a file named compose.yaml, though docker-compose.yml is still supported for compatibility. In this guide, we will use docker-compose.yml because many readers still recognize that name first.
A Compose file describes your application’s environment using three top-level keys:
-
services— defines each container: its image, ports, volumes, environment variables, and dependencies -
volumes— declares named volumes that persist data across container restarts -
networks— defines custom networks for service communication (Compose creates a default network automatically)
Compose V2 does not require a version: field at the top of the file. If you see version: '3' in older guides, that is legacy syntax kept for backward compatibility, not something new Compose files need.
YAML uses indentation to define structure. Use spaces, not tabs.
Setting Up the Example
In this example, we will run a SvelteKit development server alongside a PostgreSQL 16 database using Docker Compose.
Start by creating a new SvelteKit project. The npm create command launches an interactive wizard — select “Skeleton project” when prompted, then choose your preferred options for TypeScript and linting:
npm create svelte@latest myapp
cd myappIf you already have a Node.js project, skip this step and use your project directory instead.
Next, create a docker-compose.yml file in the project root:
services:
app:
image: node:20-alpine
working_dir: /app
volumes:
- .:/app
- node_modules:/app/node_modules
ports:
- "5173:5173"
environment:
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/myapp
depends_on:
db:
condition: service_healthy
command: sh -c "npm install && npm run dev -- --host"
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
node_modules:Compose V2 automatically reads a .env file in the project directory and substitutes ${VAR} references in the compose file. Create a .env file to define the database password:
POSTGRES_PASSWORD=changeme.env to version control. Add it to your .gitignore file to keep credentials out of your repository.The password above is for local development only. We will walk through each directive in the compose file in the next section.
Understanding the Compose File
Let us go through each directive in the compose file.
services
The services key is the core of any compose file. Each entry under services defines one container. The key name (app, db) becomes the service name, and Compose also uses it as the hostname for inter-service communication — so the app container can reach the database at db:5432.
image
The image directive tells Compose which Docker image to pull. Both node:20-alpine and postgres:16-alpine are pulled from Docker Hub if they are not already present on your machine. The alpine variant uses Alpine Linux as a base, which keeps image sizes small.
working_dir
The working_dir directive sets the working directory inside the container. All commands run in /app, which is where we mount the project files.
volumes
The app service uses two volume entries:
- .:/app
- node_modules:/app/node_modulesThe first entry is a bind mount. It maps the current directory on your host (.) to /app inside the container. Any file you edit on your host is immediately reflected inside the container, which is what enables hot-reload during development.
The second entry is a named volume. Without it, the bind mount would overwrite /app/node_modules with whatever is in your host directory — which may be empty or incompatible. The node_modules named volume tells Docker to keep a separate copy of node_modules inside the container so the bind mount does not interfere with it.
The db service uses a named volume for its data directory:
- postgres_data:/var/lib/postgresql/dataThis ensures that your database data persists across container restarts. When you run docker compose down, the postgres_data volume is kept. Only docker compose down -v removes it.
Named volumes must be declared at the top level under volumes:. Both postgres_data and node_modules are listed there with no additional configuration, which tells Docker to manage them using its default storage driver.
ports
The ports directive maps ports between the host machine and the container in "HOST:CONTAINER" format. The entry "5173:5173" exposes the Vite development server so you can open http://localhost:5173 in your browser.
environment
The environment directive sets environment variables inside the container. The app service receives the DATABASE_URL connection string, which a SvelteKit application can read to connect to the database.
Notice the ${POSTGRES_PASSWORD} syntax. Compose reads this value from the .env file and substitutes it before starting the container. This keeps credentials out of the compose file itself.
depends_on
By default, depends_on only controls the order in which containers start — it does not wait for a service to be ready. Compose V2 supports a long-form syntax with a condition key that changes this behavior:
depends_on:
db:
condition: service_healthyWith condition: service_healthy, Compose waits until the db service passes its healthcheck before starting app. This prevents the Node.js process from trying to connect to PostgreSQL before the database is accepting connections.
command
The command directive overrides the default command defined in the image. Here it runs npm install to install dependencies, then starts the Vite dev server with --host to bind to 0.0.0.0 inside the container (required for Docker to forward the port to your host):
sh -c "npm install && npm run dev -- --host"Running npm install on every container start is slow but convenient for development — you do not need to install dependencies manually before running docker compose up. For production, use a multi-stage Dockerfile
to pre-install dependencies and build the application.
healthcheck
The healthcheck directive defines a command Compose runs inside the container to determine whether the service is ready. The pg_isready utility checks whether PostgreSQL is accepting connections:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5Compose runs this check every 5 seconds. Once the service passes its first check, it is marked as healthy and any services using condition: service_healthy are allowed to start. If the check fails 5 consecutive times, the service is marked as unhealthy.
Managing the Application
Run all commands from the project directory — the directory where your docker-compose.yml is located.
Starting Services
To start all services and stream their logs to your terminal, run:
docker compose upCompose pulls any missing images, creates the network, starts the db container, waits for it to pass its healthcheck, then starts the app container. Press Ctrl+C to stop.
To start in detached mode and run the services in the background:
docker compose up -dViewing Status and Logs
To list running services and their current state:
docker compose psNAME IMAGE COMMAND SERVICE STATUS PORTS
myapp-app-1 node:20-alpine "docker-entrypoint.s…" app Up 2 minutes 0.0.0.0:5173->5173/tcp
myapp-db-1 postgres:16-alpine "docker-entrypoint.s…" db Up 2 minutes 5432/tcp
To view the logs for a specific service:
docker compose logs appTo follow the logs in real time (like tail -f):
docker compose logs -f appPress Ctrl+C to stop following.
Running Commands Inside a Container
To open an interactive shell inside the running app container:
docker compose exec app shThis is useful for running one-off commands, inspecting the file system, or debugging. Type exit to leave the shell.
Stopping and Removing Services
To stop running containers without removing them — preserving named volumes and the network:
docker compose stopTo start them again after stopping:
docker compose startTo stop containers and remove them along with the network:
docker compose downNamed volumes (postgres_data, node_modules) are preserved. Your database data is safe.
To also remove all named volumes — this deletes your database data:
docker compose down -vUse this when you want a completely clean environment.
Common Directives Reference
The following directives are not used in the example above but are commonly needed in real projects.
restart
The restart directive controls what Compose does when a container exits:
services:
app:
restart: unless-stoppedThe available policies are:
-
no— do not restart (default) -
always— always restart, including on system reboot -
unless-stopped— restart unless the container was explicitly stopped -
on-failure— restart only when the container exits with a non-zero status
Use unless-stopped for long-running services in single-host deployments.
build
Instead of pulling a pre-built image, build tells Compose to build the image from a local Dockerfile
:
services:
app:
build:
context: .
dockerfile: Dockerfilecontext is the directory Compose sends to the Docker daemon as the build context. dockerfile specifies the Dockerfile path relative to context. If both the image and a build context exist, build takes precedence.
env_file
The env_file directive injects environment variables into a container from a file at runtime:
services:
app:
env_file:
- .env.localThis is different from Compose’s native .env substitution. The .env file at the project root is read by Compose itself to substitute ${VAR} references in the compose file. The env_file directive injects variables directly into the container’s environment. Both can be used together.
networks
Compose creates a default bridge network connecting all services automatically. You can define custom networks to isolate groups of services or control how containers communicate:
services:
app:
networks:
- frontend
- backend
db:
networks:
- backend
networks:
frontend:
backend:A service only communicates with other services on the same network. In this example, app can reach db because both share the backend network. A service attached only to frontend — not backend — has no route to db.
profiles
Profiles let you define optional services that only start when explicitly requested. This is useful for development tools, debuggers, or admin interfaces that you do not want running all the time:
services:
app:
image: node:20-alpine
adminer:
image: adminer
profiles:
- tools
ports:
- "8080:8080"Running docker compose up starts only app. To also start adminer, pass the profile flag:
docker compose --profile tools upTroubleshooting
Port is already in use
Another process on your host is using the same port. Change the host-side port in the ports: mapping. For example, change "5173:5173" to "5174:5173" to expose the container on port 5174 instead.
Service cannot connect to the database
If you are not using condition: service_healthy, depends_on only ensures the db container starts before app — it does not wait for PostgreSQL to be ready to accept connections. Add a healthcheck to the db service and set condition: service_healthy in depends_on as shown in the example above.
node_modules is empty inside the container
The bind mount .:/app maps your host directory to /app, which overwrites /app/node_modules with whatever is on your host. If your host directory has no node_modules, the container sees none either. Fix this by adding a named volume entry node_modules:/app/node_modules as shown in the example. Docker preserves the named volume’s contents and the bind mount does not overwrite it.
FAQ
What is the difference between docker compose down and docker compose stop?docker compose stop stops the running containers but leaves them on disk along with the network and volumes. You can restart them with docker compose start. docker compose down removes the containers and the network. Named volumes are kept unless you add the -v flag.
How do I view logs for a specific service?
Run docker compose logs SERVICE, replacing SERVICE with the service name defined in your compose file (for example, docker compose logs app). To follow logs in real time, add the -f flag: docker compose logs -f app.
How do I pass secrets without hardcoding them in the Compose file?
Create a .env file in the project directory with your credentials (for example, POSTGRES_PASSWORD=changeme) and reference them in the compose file using ${VAR} syntax. Compose reads the .env file automatically. Add .env to your .gitignore so credentials are never committed to version control.
Can I use Docker Compose in production?
Compose works well for single-host deployments — it is simpler to operate than Kubernetes when you only have one server. Use restart: unless-stopped to keep services running after reboots, and keep secrets in environment variables rather than hardcoded values. For multi-host deployments, look at Docker Swarm or Kubernetes.
What is the difference between a bind mount and a named volume?
A bind mount (./host-path:/container-path) maps a specific directory from your host into the container. Changes on either side are immediately visible on the other. A named volume (volume-name:/container-path) is managed entirely by Docker — it persists data independently of the host directory structure and is not tied to a specific path on your machine. Use bind mounts for source code (so edits take effect immediately) and named volumes for database data (so it persists reliably).
Conclusion
Docker Compose gives you a straightforward way to define and run multi-container development environments with a single YAML file. Once you are comfortable with the basics, the next step is writing custom Dockerfiles to build your own images instead of relying on generic ones.
