What Is Docker Compose
Docker Compose is a tool for defining and running multi-container Docker applications. Instead of starting each container manually with long docker run commands, you describe your entire application stack in a single YAML file and bring it up with one command.
A typical web application needs at least three services: a web server, an application runtime, and a database. Without Compose, you would need to create a network, start each container in the right order, connect them to the network, map ports, mount volumes, and pass environment variables — all through separate CLI commands. With Compose, you declare all of that in docker-compose.yml and run docker compose up.
Docker Compose is built into Docker Desktop and available as a standalone plugin for Docker Engine on Linux. Since Docker Compose V2 (2023), the command is docker compose (with a space), not docker-compose (with a hyphen). The old V1 syntax still works, but V2 is the standard going forward.
The top-level version key in docker-compose.yml (e.g., version: "3.8") is now optional and ignored by Docker Compose V2. You can omit it entirely. This guide uses the latest specification without a version key.
The docker-compose.yml File Structure
A Compose file has four top-level keys. You will use services in every project. The other three are needed when your stack grows beyond a single service with default settings.
services: # Required. Defines each container in your application.
web:
image: nginx:alpine
api:
build: ./api
db:
image: postgres:16
networks: # Optional. Custom networks for service isolation.
frontend:
backend:
volumes: # Optional. Named volumes for persistent data.
db-data:
cache-data:
configs: # Optional. External configuration files.
nginx-conf:
file: ./nginx.conf
The file must be valid YAML. Indentation matters — use two spaces per level (not tabs). If you are unsure about your YAML syntax, paste it into QTool's YAML Formatter to validate and auto-format it before running docker compose up.
Defining Services
Each key under services creates a container. A service can use a pre-built image from Docker Hub or build from a Dockerfile in your project.
Using a Pre-Built Image
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
restart: unless-stopped
Building from a Dockerfile
services:
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "3000:3000"
volumes:
- ./api/src:/app/src
restart: unless-stopped
Common Service Properties
| Property | Purpose | Example |
|---|---|---|
image |
Docker image to use | nginx:1.25-alpine |
build |
Path to Dockerfile | ./app or {context: ./app, dockerfile: Dockerfile.prod} |
ports |
Map host:container ports | "8080:80" |
volumes |
Mount directories or named volumes | ./data:/app/data |
environment |
Set environment variables | NODE_ENV: production |
restart |
Restart policy | unless-stopped, always, on-failure |
command |
Override default command | ["npm", "run", "dev"] |
depends_on |
Startup order | [db, redis] |
networks |
Attach to networks | [frontend, backend] |
Networks: Service Communication
Docker Compose creates a default network for your project automatically. All services can reach each other by their service name as a hostname. If your Compose file defines services api and db, the API container can connect to the database at db:5432 without any extra configuration.
Custom networks let you isolate services. A common pattern is separating your frontend-facing services from your backend-only services.
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
networks:
- frontend
api:
build: ./api
networks:
- frontend
- backend
db:
image: postgres:16
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # No external access
In this setup, nginx can reach api (both on frontend), and api can reach db (both on backend). But nginx cannot reach db directly — they share no network. The internal: true flag on the backend network also blocks outbound internet access from the database container.
Within a Docker Compose network, services discover each other by name. The service name in your Compose file becomes the DNS hostname. Use it in your connection strings: postgres://user:pass@db:5432/mydb, redis://redis:6379, http://api:3000.
Volumes: Persistent Data
Containers are ephemeral by default. When a container is removed, its filesystem is gone. Volumes solve this by storing data outside the container lifecycle.
Named Volumes
Named volumes are managed by Docker. They persist until explicitly deleted with docker volume rm. Use them for database data, uploaded files, and application state.
services:
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
db-data:
redis-data:
Bind Mounts
Bind mounts map a host directory into the container. Changes on either side are reflected immediately. This is essential for development workflows where you want hot reload without rebuilding the image.
services:
api:
build: ./api
volumes:
- ./api/src:/app/src # Source code (live reload)
- ./api/package.json:/app/package.json
- /app/node_modules # Anonymous volume (prevents overwrite)
command: ["npm", "run", "dev"]
The anonymous volume /app/node_modules (no host path before the colon) prevents your local node_modules from overwriting the container's installed dependencies. This is a critical pattern when using bind mounts with Node.js projects.
Environment Variables and Secrets
There are three ways to pass environment variables to your containers. Each has different use cases.
1. Inline in docker-compose.yml
services:
api:
image: node:20-alpine
environment:
NODE_ENV: production
PORT: 3000
LOG_LEVEL: info
2. Using an .env File
services:
api:
image: node:20-alpine
env_file:
- ./api/.env
- ./api/.env.local # Overrides values from .env
DATABASE_URL=postgres://user:pass@db:5432/myapp
REDIS_URL=redis://redis:6379
JWT_SECRET=your-secret-key-here
SMTP_HOST=smtp.example.com
SMTP_PORT=587
3. Variable Interpolation
Use ${VARIABLE} syntax in your Compose file to reference values from your shell or from a .env file in the project root.
services:
db:
image: postgres:${POSTGRES_VERSION:-16}
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "${DB_PORT:-5432}:5432"
The :- syntax provides a default value. ${POSTGRES_VERSION:-16} uses 16 if POSTGRES_VERSION is not set. This makes your Compose file flexible across environments.
To convert environment variables between formats (e.g., from .env to JSON for your application config), try the YAML to JSON or JSON Formatter tools.
Never commit .env files with real passwords or API keys to version control. Add .env to your .gitignore. For production, use Docker secrets or your cloud provider's secret management service. Encode sensitive values with Base64 if needed for transport, but remember that Base64 is encoding, not encryption.
Build Context and Multi-Stage Builds
The build key controls how Docker Compose builds your images. At its simplest, you point it at a directory containing a Dockerfile.
services:
api:
build:
context: ./api # Directory with Dockerfile
dockerfile: Dockerfile.prod # Non-default Dockerfile name
target: production # Multi-stage build target
args:
NODE_VERSION: 20
BUILD_DATE: 2026-02-14
cache_from:
- myapp-api:latest # Use existing image as cache
Multi-Stage Dockerfile
Multi-stage builds produce smaller production images by separating the build environment from the runtime environment.
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
Your Compose file targets the correct stage:
services:
# Development: uses builder stage with source mount
api-dev:
build:
context: ./api
target: builder
volumes:
- ./api/src:/app/src
command: ["npm", "run", "dev"]
# Production: uses production stage
api:
build:
context: ./api
target: production
restart: unless-stopped
Health Checks and depends_on
depends_on controls startup order, but by default it only waits for the container to start — not for the application inside to be ready. A PostgreSQL container might take several seconds to initialize its data directory after the container is running.
Combine healthcheck with depends_on: condition: service_healthy to wait for actual readiness.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
volumes:
- db-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
api:
build: ./api
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/myapp
REDIS_URL: redis://redis:6379
volumes:
db-data:
Now api will not start until both db and redis pass their health checks. The start_period gives slow-starting services (like databases) time to initialize before health checks count as failures.
Health Check Properties
| Property | Default | Purpose |
|---|---|---|
test |
— | Command to run. Exit code 0 = healthy. |
interval |
30s | Time between health checks. |
timeout |
30s | Max time for a single check. |
retries |
3 | Consecutive failures before unhealthy. |
start_period |
0s | Grace period for slow-starting containers. |
Essential Docker Compose Commands
These are the commands you will use daily. All of them are run from the directory containing your docker-compose.yml file.
# Start all services (detached mode)
docker compose up -d
# Start and force rebuild images
docker compose up -d --build
# Stop all services (containers remain)
docker compose stop
# Stop and remove containers, networks
docker compose down
# Stop, remove containers, AND delete volumes (CAUTION: data loss)
docker compose down -v
# Restart a specific service
docker compose restart api
# List running services
docker compose ps
# View logs (all services)
docker compose logs
# Follow logs for a specific service
docker compose logs -f api --tail 100
# Show running processes
docker compose top
# Validate compose file
docker compose config
# Open a shell in a running container
docker compose exec api sh
# Run a one-off command (creates a new container)
docker compose run --rm api npm test
# Scale a service to multiple instances
docker compose up -d --scale worker=3
Use docker compose config before every up to catch YAML syntax errors and unresolved variables. It outputs the fully resolved configuration, showing you exactly what Docker will use. You can also validate your YAML syntax with the YAML Formatter for quick error checking.
Production Best Practices
Development Compose files are not production-ready. Here are the changes you need to make before deploying.
1. Use Specific Image Tags
# Bad: unpredictable
image: postgres:latest
# Good: pinned version
image: postgres:16.2-alpine
2. Set Resource Limits
services:
api:
build: ./api
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
3. Configure Restart Policies
services:
api:
restart: unless-stopped # Restarts unless manually stopped
worker:
restart: on-failure # Only restart on non-zero exit
deploy:
restart_policy:
condition: on-failure
max_attempts: 5
delay: 10s
4. Use Read-Only Filesystems
services:
api:
read_only: true
tmpfs:
- /tmp
- /app/logs
security_opt:
- no-new-privileges:true
5. Separate Dev and Prod Files
Use a base file and environment-specific overrides:
# Development
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
services:
api:
build:
target: production
environment:
NODE_ENV: production
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
6. Configure Logging
Docker stores container logs on disk by default with no size limit. In production, this can fill your disk.
services:
api:
logging:
driver: json-file
options:
max-size: "10m" # Rotate after 10MB
max-file: "5" # Keep 5 rotated files
Full-Stack Example
Here is a production-ready Compose file for a typical web application with Nginx, a Node.js API, PostgreSQL, and Redis.
services:
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
api:
condition: service_healthy
networks:
- frontend
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "5m"
max-file: "3"
api:
build:
context: ./api
target: production
expose:
- "3000"
env_file:
- ./api/.env
environment:
NODE_ENV: production
DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
REDIS_URL: redis://redis:6379
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- frontend
- backend
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
db:
image: postgres:16.2-alpine
volumes:
- db-data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
networks:
- backend
restart: unless-stopped
deploy:
resources:
limits:
memory: 256M
redis:
image: redis:7.2-alpine
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "128mb"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
networks:
- backend
restart: unless-stopped
deploy:
resources:
limits:
memory: 192M
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true
volumes:
db-data:
redis-data:
This configuration isolates the database and Redis on an internal network, limits memory usage for each service, includes health checks for proper startup ordering, and uses read-only volume mounts for configuration files. To compare this file with your own configuration, use the Diff Checker to spot differences side by side.
Validate Your YAML Before Deploying
Catch syntax errors, fix indentation, and convert between formats. QTool's YAML tools work entirely in your browser — your config never leaves your machine.
Open YAML Formatter YAML to JSONRelated Tools
Frequently Asked Questions
docker compose up creates and starts containers, networks, and volumes as defined in your docker-compose.yml file. If containers do not exist, it builds images (if build context is specified), creates the containers, and starts them. docker compose start only starts existing containers that were previously created but stopped. If the containers do not exist yet, docker compose start will fail. In most workflows you use docker compose up (with the -d flag for detached mode) to bring your entire stack online, and docker compose start only if you previously ran docker compose stop to pause services without removing them.
Docker Compose supports multiple methods for passing environment variables. You can define them inline in docker-compose.yml using the environment key with key-value pairs. You can reference a .env file using the env_file key, which loads all variables from that file into the container. You can also use variable interpolation in docker-compose.yml with ${VARIABLE_NAME} syntax, which pulls values from your shell environment or a .env file in the same directory as your compose file. For sensitive values like API keys and passwords, use Docker secrets in production or store them in a .env file that is excluded from version control via .gitignore.
depends_on controls the startup order of services in Docker Compose. If service B depends_on service A, Compose will start A before B. However, depends_on only waits for the container to start, not for the application inside it to be ready. A database container might be running but still initializing its data. To wait for actual readiness, use depends_on with a condition set to service_healthy combined with a healthcheck on the dependency. For example, a PostgreSQL service can have a healthcheck that runs pg_isready, and the dependent service uses condition: service_healthy to wait until the database actually accepts connections.
Docker Compose volumes persist data outside of containers so it survives container restarts and removals. There are two types: named volumes and bind mounts. Named volumes (defined in the top-level volumes section) are managed by Docker and stored in Docker's internal storage. They are best for database data, application state, and anything that should persist independently of your host filesystem. Bind mounts map a host directory directly into the container (e.g., ./src:/app/src) and are best for development, where you want live code changes reflected in the container without rebuilding. Use named volumes for production data and bind mounts for development source code.
Yes, Docker Compose can be used in production for single-host deployments. It is well suited for small to medium applications running on a single server. For production use, add restart policies (restart: unless-stopped), configure health checks, use named volumes for persistent data, set resource limits (memory and CPU), avoid bind mounts, use specific image tags instead of latest, and store secrets securely. For multi-host deployments with load balancing, automatic failover, and horizontal scaling, consider Docker Swarm or Kubernetes instead. Many teams use Docker Compose for development and staging, then deploy to Kubernetes in production.
Use docker compose logs to view output from all services, or docker compose logs [service-name] for a specific service. Add -f to follow logs in real time (similar to tail -f). Add --tail 100 to see only the last 100 lines. For debugging, docker compose exec [service-name] sh opens a shell inside a running container. docker compose ps shows the status of all services. docker compose top shows running processes. If a container keeps crashing, check exit codes with docker compose ps -a and inspect logs for error messages. You can also use docker compose config to validate your docker-compose.yml file before starting services.