Running docker run manually with ten different flags (-p, -v, -e, --network) works for a single container. But a real application rarely consists of a single container.
A modern SaaS application might require:
A Next.js frontend
A Node.js API
A PostgreSQL database
A Redis cache
Typing out four complex docker run commands in the exact right order every time you want to develop locally is unsustainable.
Docker Compose is an orchestration tool that allows you to define your entire multi-container environment in a single declarative YAML file. In this module, we will explore Compose v2, managing boot order, and utilizing overrides for different environments.
The Reality of Compose V2
If you learned Docker years ago, you probably used docker-compose (with a hyphen). This was Compose V1, written in Python.
Docker has since completely rewritten Compose in Go and integrated it directly into the Docker CLI as a plugin.
Old Way:docker-compose up
New Way:docker compose up
Similarly, the configuration file is now officially named compose.yaml (though docker-compose.yml is still supported for backwards compatibility). Always prefer Compose V2 and compose.yaml for new projects.
Anatomy of a compose.yaml
A compose.yaml file defines services, networks, and volumes.
Here is a complete local development environment for a full-stack application:
yaml
What Compose does for you:
When you run docker compose up, Compose automatically:
Creates a custom bridge network named my-saas-app_default.
Creates the pgdata volume if it doesn't exist.
Builds the api image from the ./api directory.
Starts all containers and attaches them to the custom network, enabling internal DNS (the api can connect to db:5432).
Streams the interleaved logs from all three containers to your terminal.
Managing Boot Order (depends_on)
In the example above, the api container will crash immediately if the db container isn't ready to accept connections.
A common mistake is using a simple depends_on:
yaml
This only tells Compose to start the db container before the api container. But PostgreSQL takes several seconds to initialize its internal data directory before it can accept TCP connections on port 5432. The api container will still crash!
The robust solution: Use Healthchecks.
Define a healthcheck on the database service (using pg_isready).
Tell the API to wait until that healthcheck passes:
yaml
Now, Compose will start the database, continuously poll pg_isready, and only launch the API after Postgres confirms it is fully booted.
The Anonymous Volume Trick (node_modules)
Look closely at the api volumes:
yaml
If you bind-mount your local ./api directory into /app to enable hot-reloading, you will accidentally overwrite the container's Linux-compiled node_modules with your host machine's macOS/Windows node_modules. This breaks native C++ bindings (like bcrypt or sharp).
The second volume (- /app/node_modules) is an Anonymous Volume. It tells Docker: "Bind mount the whole /app folder, but except the node_modules directory. Leave the container's version of that folder intact."
This is a critical trick for local Node.js development in Compose.
Overrides for Different Environments
You should never use bind-mounts or target dev build stages in production.
Compose allows you to define a base compose.yaml with your core services, and then layer override files on top of it.
By default, running docker compose up will read compose.yaml and then automatically merge compose.override.yaml on top of it.
You can define production settings in a separate file (e.g., compose.prod.yaml) and apply them manually:
bash
This allows you to maintain a single source of truth for your architecture while hot-swapping development and production configurations.
Key Takeaways
Compose V2: Use docker compose (no hyphen) and name your file compose.yaml.
Implicit Networking: Compose automatically creates a user-defined bridge network, enabling easy DNS resolution between services.
Boot Order: Use depends_on: condition: service_healthy coupled with a database healthcheck to prevent your Node.js app from crashing on startup.
Anonymous Volumes: Use them to protect the container's node_modules from being overwritten by a host bind-mount during local development.
Knowledge Check
You have defined depends_on: [ "db" ] for your Node.js API service in compose.yaml. However, the Node.js API still crashes on startup, complaining that the database connection failed. Why?
When configuring volumes for a Node.js API in compose.yaml for local development, you include two volume definitions: - ./api:/app and - /app/node_modules. What is the specific architectural purpose of the second "Anonymous Volume" definition?
Your team uses a compose.yaml file that includes bind mounts and targeting a dev build stage for local development. However, these settings should never be used in production. What is the Docker Compose best practice for managing these environment-specific differences?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
# compose.yamlname: my-saas-app
services:# 1. The PostgreSQL Databasedb:image: postgres:15-alpine
environment:POSTGRES_USER: admin
POSTGRES_PASSWORD: secretpassword
POSTGRES_DB: saas_db
ports:-"5432:5432"volumes:- pgdata:/var/lib/postgresql/data
healthcheck:test:["CMD-SHELL","pg_isready -U admin -d saas_db"]interval: 5s
timeout: 5s
retries:5# 2. The Redis Cachecache:image: redis:7-alpine
ports:-"6379:6379"# 3. The Node.js APIapi:build:context: ./api
target: dev # Target a specific stage in a multi-stage Dockerfileenvironment:DATABASE_URL: postgres://admin:secretpassword@db:5432/saas_db
REDIS_URL: redis://cache:6379ports:-"8080:8080"volumes:- ./api:/app # Bind mount for hot-reloading- /app/node_modules # Anonymous volume to protect container's node_modulesdepends_on:db:condition: service_healthy
cache:condition: service_started
volumes:pgdata:# Declare the named volume used by the db service
depends_on:- db
depends_on:db:condition: service_healthy
volumes:- ./api:/app
- /app/node_modules
docker compose -f compose.yaml -f compose.prod.yaml up -d