Docker Compose Cheat Sheet
Orchestrate multi-container applications with ease. Docker Compose is the modern way to run local development environments and define infrastructure as code.
🛠️ CLI Commands
docker-compose up -dBuilds, (re)creates, starts, and attaches to containers for all services. -d runs in detached mode (background).
docker-compose downStops and removes containers, networks, and optionally images/volumes created by `up`.
docker-compose logs -fView combined output from all services. -f follows the log output in real-time.
docker-compose buildRebuild service images. Useful when you have changed the Dockerfile but not the compose file.
docker-compose psList containers running as part of this Compose project.
docker-compose exec service_name bashExecute a command in a running service container. Opens an interactive shell.
📄 YAML File Reference
A comprehensive docker-compose.yml file structure with common configurations.
version: '3.9' # Compose file format version
services:
web:
build: . # Build from Dockerfile in current dir
ports:
- "5000:5000" # Map host:container ports
volumes:
- .:/code # Bind mount for live code reload
- node_modules:/code/node_modules # Named volume
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://db:5432
depends_on:
- db
- redis
restart: unless-stopped # Restart policy
redis:
image: "redis:7-alpine" # Use official image
expose:
- "6379" # Internal only, not mapped to host
db:
image: postgres:15
volumes:
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data: # Named volume for persistence
node_modules: # Named volume for node_modules
networks:
default:
driver: bridgeUnderstanding Docker Compose: The Complete Guide
Docker Compose solves one of the biggest challenges in modern software development: managing multi-container applications. While a simple application might run in a single container, real-world applications typically consist of multiple interconnected services—a web server, a database, a cache, a message queue, and more. Manually running docker run commands for each service, configuring networking between them, and ensuring they start in the right order becomes unmanageable very quickly.
Docker Compose addresses this by allowing you to define your entire application stack in a single YAML file. With one command—docker-compose up—you can create and start all the services, configure networking so they can communicate, mount volumes for data persistence, and set environment variables. This is not just convenient; it is reproducible. Any developer can clone your repository and have the exact same development environment running in minutes.
The Evolution of Docker Compose
Docker Compose has evolved significantly over the years. Originally, it was a separate tool that needed to be installed independently of Docker. The command was docker-compose (with a hyphen). In newer Docker versions, Compose is integrated directly into the Docker CLI as a plugin, and the command is docker compose (with a space). Both syntaxes work, and the Compose file format is the same, but you may encounter either form in documentation and tutorials.
The Compose file specification has also evolved. Version 2 introduced the version key and multi-file support. Version 3 added support for Docker Swarm deployment. In the latest specifications, the version key is actually optional—Docker automatically uses the latest format. However, many teams still include it for clarity, especially when specific version features are used.
Key Concepts in Docker Compose
A Compose file defines services, which are the containers that make up your application. Each service specifies either an image (to pull from a registry) or a build context (to build from a Dockerfile). Services can depend on each other, share networks, mount volumes, and expose ports.
Networks in Compose handle inter-container communication. By default, Compose creates a single network for your application, and all services join it. Containers can reach each other using their service names as hostnames—the web service can connect to the database at db:5432 without knowing any IP addresses. You can also define custom networks for more complex isolation requirements.
Volumes provide data persistence. Without volumes, all data written inside a container is lost when the container is removed. Named volumes (defined in the top-level volumes section) are managed by Docker and persist across container recreations. Bind mounts (direct paths like ./data:/app/data) map host directories into containers, which is useful for development but creates host dependency.
Essential Compose Commands
The most important command is docker-compose up. It reads the compose file, pulls any missing images, builds any services with build contexts, creates containers, and starts them. The -d flag (detached mode) runs containers in the background and returns control to your terminal. Without it, logs from all services stream to your terminal, and Ctrl+C stops everything.
docker-compose down is the counterpart that stops and removes containers, networks, and the default network. Add -v to also remove volumes (this deletes all persistent data, so use carefully). Add --rmi all to remove images as well.
docker-compose logs shows output from your services. Add -f to follow logs in real-time (like tail -f). Add a service name to see logs from just that service: docker-compose logs -f web.
docker-compose exec service_name command runs a command inside a running container. This is how you get a shell into a container (exec web bash) or run database migrations (exec web python manage.py migrate). It requires the service to already be running.
docker-compose run service_name command is similar but creates a new container to run the command, useful for one-off tasks that should not affect the running service.
Best Practices for Compose Files
Use environment variables for configuration that changes between environments. In development, you might connect to a local database; in production, to a managed cloud database. Define a .env file for local defaults and use variable substitution syntax in the compose file. Docker Compose automatically loads .env files.
Order services logically in your compose file, typically with the most dependent services (like the main application) at the top and infrastructure services (databases, caches) below. Use depends_on to ensure services start in the right order, but be aware that this only waits for the container to start, not for the service inside to be ready.
For services that need to wait for dependencies to be truly ready (like waiting for a database to accept connections), use healthchecks. Define a healthcheck for the dependency service, then use depends_on with the condition: service_healthy option.
Keep development and production configurations separate. Use a base docker-compose.yml for shared configuration and override files like docker-compose.override.yml (loaded automatically) for development-specific settings or docker-compose.prod.yml for production (loaded explicitly with -f).
Common Patterns and Solutions
For Node.js applications with bind-mounted source code, you often want node_modules to live in a named volume rather than being synced from the host. This prevents platform-specific compiled modules from conflicting and improves performance. The pattern is to bind mount the project root and then "shadow" node_modules with a volume.
For database initialization, most official database images support placing SQL scripts in a specific directory (like /docker-entrypoint-initdb.d/) to run on first startup. Mount your init scripts there to seed your development database automatically.
For live code reloading during development, use bind mounts for your source code and make sure your application is configured to reload on file changes. Most modern frameworks (Node with nodemon, Python with reload, Rails with spring) support this out of the box.
Debugging Common Issues
If containers keep restarting, check logs with docker-compose logs service_name. The most common causes are missing environment variables, unavailable dependencies (database not ready yet), or application errors during startup.
If services cannot communicate, verify they are on the same network (check with docker network inspect) and that you are using service names as hostnames, not "localhost" (which refers to the container itself, not other services).
If builds are slow, leverage Docker layer caching by ordering Dockerfile instructions from least to most frequently changed. Copy dependency files and install dependencies before copying application source code.