Build, validate, and debug Docker configurations without uploading anything:
- Docker Run → Compose Converter — convert docker run commands to docker-compose.yml
- Docker Compose Validator — validate docker-compose.yml syntax and semantics
- Docker Env Mapper — visualize and manage environment variable mappings
- Docker Volume Permissions Helper — fix bind mount UID/GID permission errors
- Chmod Calculator — visual Linux file permission calculator
- Chown Command Generator — generate file ownership commands
1. What is Docker Compose?
Docker Compose is a tool for defining and running multi-container Docker applications. Instead of typing long docker run commands with dozens of flags for each container, you describe your entire application stack — web servers, databases, caches, workers — in a single docker-compose.yml file and bring everything up with one command: docker compose up.
The key insight is declarative infrastructure. You don't tell Docker how to start containers step by step. You describe what your stack looks like — which services exist, what images they use, which ports they expose, how they connect — and Compose figures out the execution order, creates networks, mounts volumes, and starts everything.
Compose Specification vs Legacy Versions
Docker Compose has gone through multiple version schemes that confuse developers to this day. Here is the complete history:
| Era | CLI Command | File Format | Status in 2026 |
|---|---|---|---|
| V1 (Python) | docker-compose (hyphen) | version: "2" / "3" | ❌ EOL June 2023 |
| V2 (Go plugin) | docker compose (space) | Compose Specification | ✅ Current standard |
The version key is obsolete. As of Docker Compose v2.20+ and the Compose Specification, the version: top-level key is no longer required and is silently ignored. Remove it from new files. If you see version: "3.8" in a tutorial, that tutorial is outdated.
When to Use Docker Compose
- Local development — spin up your entire stack (app + database + cache + queue) with one command.
- CI/CD testing — create isolated test environments with real databases instead of mocks.
- Single-server deployments — run production workloads on a single VPS or dedicated server.
- Prototyping — try out new services (Elasticsearch, MinIO, Grafana) without installing anything on your host.
Docker Compose is not designed for multi-host orchestration. If you need containers distributed across multiple servers with automatic scaling, load balancing, and rolling updates, use Kubernetes or Docker Swarm. Compose is for single-host deployments.
2. Docker Run vs Docker Compose
Every docker run flag has a direct equivalent in docker-compose.yml. The difference is ergonomic: docker run is imperative (you execute commands one at a time), while Compose is declarative (you describe the desired state once and let Compose handle execution).
# docker run (imperative — one container at a time)
docker run -d \
--name my-app \
-p 3000:3000 \
-e NODE_ENV=production \
-e DATABASE_URL=postgres://app:secret@db:5432/myapp \
-v ./uploads:/app/uploads \
--network app-network \
--restart unless-stopped \
my-app:latest
# vs. Docker Compose (declarative — entire stack)
# docker compose up -d
# ↑ starts web + db + cache in one command docker run Flag | Compose Equivalent | Example |
|---|---|---|
-p 3000:3000 | ports: | - "3000:3000" |
-v ./data:/data | volumes: | - ./data:/data |
-e KEY=value | environment: | - KEY=value |
--name my-app | Service key name | services: my-app: |
--network net | networks: | - net |
--restart always | restart: | restart: always |
-d | docker compose up -d | Detached mode |
Have an existing docker run command you want to convert? Use our Docker Run to Compose Converter — paste your command and get a properly formatted docker-compose.yml instantly, entirely in your browser.
3. The docker-compose.yml File Structure
A Compose file has four top-level keys. Only services is required:
# Top-level structure of docker-compose.yml
services: # Required — defines your containers
web: ...
db: ...
volumes: # Optional — declares named volumes
pgdata: ...
networks: # Optional — declares custom networks
backend: ...
configs: # Optional — external configuration files
secrets: # Optional — sensitive data management Here is a complete, production-ready multi-service example with a Node.js web app, PostgreSQL database, and Redis cache:
# docker-compose.yml — Complete multi-service example
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://app:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
restart: unless-stopped
volumes:
- ./uploads:/app/uploads
networks:
- app-network
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- app-network
cache:
image: redis:7-alpine
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
restart: unless-stopped
networks:
- app-network
volumes:
pgdata:
networks:
app-network:
driver: bridge File Naming and Location
Docker Compose looks for files in this priority order:
compose.yml(Compose Specification standard)compose.yamldocker-compose.yml(legacy, still widely used)docker-compose.yaml
You can also specify a file explicitly with docker compose -f custom.yml up. Multiple -f flags merge files, which is useful for environment-specific overrides: docker compose -f compose.yml -f compose.prod.yml up.
4. Service Configuration Deep Dive
Each entry under services: defines a container. Here are the most important service-level keys:
image vs build
Every service needs either image: (pull a pre-built image) or build: (build from a Dockerfile). You can use both — build to create the image and image to tag it:
services:
web:
build: . # build from ./Dockerfile
image: my-app:1.2.3 # tag the built image ports
Map host ports to container ports. Always quote port mappings in YAML to prevent the parser from interpreting 80:80 as a base-60 number:
ports:
- "3000:3000" # host:container
- "127.0.0.1:8080:80" # bind to localhost only
- "5432" # random host port → container 5432 depends_on
Controls startup order. The basic form only waits for the container to start, not for the application to be ready. Use the extended form with healthchecks for reliable ordering:
# Basic (waits for container start only)
depends_on:
- db
- cache
# Extended (waits for healthy status)
depends_on:
db:
condition: service_healthy
cache:
condition: service_started restart
| Policy | Behavior | Use Case |
|---|---|---|
no | Never restart | One-off tasks, migrations |
always | Always restart, even on clean exit | Critical infrastructure |
unless-stopped | Restart unless manually stopped | ✅ Most production services |
on-failure | Restart only on non-zero exit | Workers, batch jobs |
5. Environment Variables & Secrets
Environment variable management is one of the trickiest parts of Docker Compose. There are three mechanisms, and they have a specific priority order when they overlap:
# .env file (loaded automatically by Compose)
POSTGRES_USER=app
POSTGRES_PASSWORD=super-secret-password
POSTGRES_DB=myapp
# docker-compose.yml — three ways to use env vars:
# Method 1: env_file (load from file)
services:
db:
image: postgres:16-alpine
env_file:
- .env.db
- .env.db.local # overrides .env.db
# Method 2: environment (inline)
web:
image: my-app:latest
environment:
- NODE_ENV=production
- API_KEY=${API_KEY} # interpolate from shell/.env
# Method 3: .env auto-loading (Compose interpolation)
cache:
image: redis:7-alpine
ports:
- "${REDIS_PORT:-6379}:6379" # default if not set Override Priority (Highest → Lowest)
docker compose run -e VAR=value— CLI override (highest)environment:key in compose fileenv_file:loaded files.envfile in project root (Compose interpolation only)- Host shell environment variables (lowest)
Important: The .env file in the project root is special — Compose reads it automatically for ${VARIABLE} interpolation inside docker-compose.yml. It does not automatically inject variables into containers. For that, you need env_file or environment.
Managing environment variables across multiple services and files gets complex fast. Use our Docker Env Mapper to visualize which variables go to which services, detect duplicates, and generate clean env_file configurations.
6. Volume Mounts & Permissions
Volumes are how containers persist data and share files with the host. Getting volume permissions right is the single most common source of Docker frustration. There are three volume types:
| Type | Syntax | Use Case | Survives down -v? |
|---|---|---|---|
| Bind mount | ./host/path:/container/path | Development, config files | Yes (on host) |
| Named volume | volume-name:/container/path | Database data, uploads | ❌ No |
| tmpfs | tmpfs: /tmp | Temporary data, caches | ❌ No (RAM only) |
The UID/GID Permission Problem
The #1 Docker volume issue: your container runs as user node (UID 1000), but the host files are owned by root (UID 0). The container gets "Permission denied" when trying to write. This happens because Linux file permissions are based on numeric UID/GID, not usernames:
# Problem: container runs as UID 1000, host files owned by root
# Result: "Permission denied" when writing to bind mount
# Fix 1: Match UIDs in Dockerfile
FROM node:20-alpine
RUN mkdir -p /app/uploads && chown -R node:node /app/uploads
USER node
# Fix 2: Set user in docker-compose.yml
services:
web:
image: my-app
user: "1000:1000"
volumes:
- ./uploads:/app/uploads
# Fix 3: Fix host permissions before mounting
# On the host:
sudo chown -R 1000:1000 ./uploads
sudo chmod 755 ./uploads
The permission calculation for Docker volumes involves matching host UIDs with container UIDs, setting the right chmod mode, and configuring the Dockerfile correctly. Our Docker Volume Permissions Helper generates all of this for you — the correct --user flags, Dockerfile RUN chown commands, and compose volume configurations.
Need to calculate specific Linux permission modes? Use the Chmod Calculator for visual permission selection. To generate ownership change commands, use the Chown Command Generator.
Docker Desktop vs Linux Host
On Docker Desktop (macOS/Windows), bind mounts go through a file-sharing layer (gRPC-FUSE or VirtioFS). This introduces I/O overhead — especially for node_modules directories with thousands of files. Named volumes bypass this layer and are significantly faster. On native Linux, bind mounts have no overhead because Docker accesses the host filesystem directly.
Best practice: use bind mounts for source code (you need live editing), but named volumes for node_modules and dependency directories:
services:
web:
volumes:
- .:/app # bind mount for source code
- node_modules:/app/node_modules # named volume for deps (fast)
volumes:
node_modules: 7. Docker Compose Networking
Docker Compose automatically creates a default bridge network for each project. All services in the same Compose file can reach each other by service name:
# If your compose file defines:
services:
web: ...
db: ...
cache: ...
# Then inside the 'web' container:
# - 'db' resolves to the database container's IP
# - 'cache' resolves to the Redis container's IP
# Example connection string: postgres://user:pass@db:5432/mydb For more complex setups, define custom networks to isolate groups of services:
# Custom networks for service isolation
services:
web:
networks:
- frontend
- backend # web can reach both networks
api:
networks:
- backend # api only on backend
db:
networks:
- backend # db only on backend (not exposed to frontend)
nginx:
networks:
- frontend # nginx only on frontend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # no external internet access Network Aliases and DNS
By default, a service is reachable by its service key name. You can add additional DNS names using aliases:
services:
db:
image: postgres:16-alpine
networks:
backend:
aliases:
- database
- postgres
# Now 'db', 'database', and 'postgres' all resolve to this container 8. Building Images with Compose
Compose can build images from Dockerfiles as part of the up process. This is especially useful for development workflows where you're iterating on your application code:
# Multi-stage build with Compose
services:
web:
build:
context: .
dockerfile: Dockerfile
target: production # use specific stage
args:
- NODE_VERSION=20
- BUILD_DATE=2026-06-21
cache_from:
- my-app:latest # use previous image as cache layer
# Dockerfile (multi-stage)
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production=false
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 package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"] Build Cache Optimization
Docker builds images in layers. Each instruction in your Dockerfile creates a layer. If a layer hasn't changed, Docker reuses the cached version. The order of your Dockerfile instructions directly affects build speed:
- Copy dependency files first (
package.json,requirements.txt), then install dependencies, then copy source code. This way, changing source code doesn't invalidate the dependency cache. - Use
.dockerignoreto excludenode_modules/,.git/,dist/, and other large directories from the build context. - Use
--buildflag:docker compose up -d --buildforces a rebuild. Without it, Compose reuses existing images.
9. Essential CLI Commands
These are the Docker Compose commands you will use daily:
# Start all services (detached)
docker compose up -d
# Start specific services only
docker compose up -d web db
# Stop and remove containers, networks
docker compose down
# Stop and remove everything including volumes (⚠️ data loss)
docker compose down -v
# View real-time logs
docker compose logs -f
docker compose logs -f web # specific service
# List running containers
docker compose ps
# Execute command in running container
docker compose exec web sh
docker compose exec db psql -U app -d myapp
# Restart a single service
docker compose restart web
# Rebuild images and restart
docker compose up -d --build
# Pull latest images
docker compose pull
# Validate compose file syntax
docker compose config --quiet
# Scale a service (multiple replicas)
docker compose up -d --scale worker=3 Command Cheat Sheet
| Command | Purpose |
|---|---|
docker compose up -d | Start all services in background |
docker compose down | Stop and remove containers + networks |
docker compose down -v | ⚠️ Also remove named volumes (data loss) |
docker compose logs -f | Follow real-time logs from all services |
docker compose ps | List containers with status and ports |
docker compose exec web sh | Open shell inside running container |
docker compose up -d --build | Rebuild images then restart |
docker compose config --quiet | Validate compose file syntax |
docker compose pull | Pull latest versions of all images |
10. Healthchecks & Startup Order
depends_on without healthchecks only waits for the container to start, not for the application inside to be ready. A PostgreSQL container can be "started" but still initializing its database. Your web app connects, gets "connection refused", and crashes.
Healthchecks solve this by letting Compose wait for a container to report itself as healthy before starting dependent services:
# Healthcheck patterns for common services
services:
postgres:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
mysql:
image: mysql:8
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 3
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
web:
depends_on:
postgres:
condition: service_healthy # wait for healthy, not just started
redis:
condition: service_healthy Healthcheck Parameters
| Parameter | Default | Purpose |
|---|---|---|
test | — | Command to run. Exit 0 = healthy, exit 1 = unhealthy |
interval | 30s | Time between health checks |
timeout | 30s | Max time for a single check to complete |
retries | 3 | Consecutive failures before marking unhealthy |
start_period | 0s | Grace period — failures during this time don't count |
11. Production Best Practices
Running Docker Compose in production on a single server is a legitimate deployment strategy for many applications. Follow these hardening practices:
# Production best practices
services:
web:
image: my-app:1.2.3 # pin exact version, never use :latest
restart: unless-stopped # auto-restart on crash
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
logging:
driver: json-file
options:
max-size: "10m" # rotate logs at 10MB
max-file: "3" # keep 3 rotated files
read_only: true # read-only root filesystem
tmpfs:
- /tmp # writable tmp in memory
security_opt:
- no-new-privileges:true # prevent privilege escalation Production Checklist
- ✅ Pin image versions —
postgres:16.3-alpine, never:latest - ✅ Set resource limits — prevent any container from consuming all host memory/CPU
- ✅ Configure log rotation — without limits, Docker logs grow until they fill the disk
- ✅ Use
restart: unless-stopped— auto-recover from crashes and reboots - ✅ Enable read-only root filesystem — prevents attackers from modifying binaries
- ✅ Use
no-new-privileges— prevents privilege escalation inside containers - ✅ Separate secrets from config — use
env_filewith.gitignore - ✅ Back up named volumes —
docker run --rm -v pgdata:/data -v $(pwd):/backup alpine tar czf /backup/pgdata.tar.gz /data
12. Validation & Troubleshooting
Docker Compose YAML errors are notoriously frustrating. A single wrong indentation, an unquoted port mapping, or a missing image: directive can cause silent failures. Here are the most common issues and how to debug them:
# Validate compose file (catches YAML and schema errors)
docker compose config --quiet
# No output = valid. Errors print to stderr.
# Check which compose file is being used
docker compose config --format json | head -5
# Debug container startup failures
docker compose logs web --tail 50
docker compose events
# Inspect a container's environment
docker compose exec web env
# Check port bindings
docker compose port web 3000
# View resource usage
docker compose top
docker stats
# Force recreate containers (even if config unchanged)
docker compose up -d --force-recreate
# Remove orphaned containers (from removed services)
docker compose up -d --remove-orphans Common Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
| yaml: line X: did not find expected key | Wrong indentation | Check for mixed tabs/spaces. Use 2-space indent. |
| service "web" has neither an image nor a build context | Missing image: or build: | Add one of them to the service definition. |
| port is already allocated | Host port conflict | Change host port or stop conflicting process. |
| permission denied | Volume UID/GID mismatch | Fix with --user, Dockerfile chown, or host chown. |
| network X not found | Missing network declaration | Add networks: section at top level. |
| depends_on: service "db" is not healthy | No healthcheck or failing healthcheck | Add healthcheck to the dependent service. |
For instant validation of your docker-compose.yml files without running Docker, use our Docker Compose Validator. It parses your YAML, checks for semantic errors (missing images, unquoted ports, invalid environment formats), and gives you a structural audit — entirely in your browser.
Frequently Asked Questions
- What is the difference between Docker Compose v1 and v2?
- Docker Compose v1 was a standalone Python binary (
docker-composewith a hyphen). V2 is a Go plugin integrated into the Docker CLI (docker composewith a space). V2 is faster, supports BuildKit by default, and uses the Compose Specification. V1 reached end-of-life in June 2023. - What is the difference between env_file and environment?
- The
environmentkey defines variables inline in the YAML file.env_fileloads them from an external file. If both define the same variable,environmentwins. Useenv_filefor secrets you don't want in version control, andenvironmentfor non-sensitive defaults. - How do I fix "permission denied" with Docker volumes?
- Permission errors happen when the container's UID doesn't match the host file ownership. Fix by: 1) Adding
user: "1000:1000"in compose, 2) Runningchownin your Dockerfile, 3) Fixing host permissions withsudo chown -R 1000:1000 ./data. Use the Docker Volume Permissions Helper to generate the correct commands. - Should I use the version key in docker-compose.yml?
- No. The
version:key is obsolete as of Compose v2.20+. The Compose Specification ignores it. Remove it from new files. Old tutorials withversion: "3.8"are outdated. - What is the difference between bind mounts and named volumes?
- Bind mounts map a specific host path to a container path. Named volumes are managed by Docker in
/var/lib/docker/volumes/. Use bind mounts for development (live code editing) and named volumes for production data (databases), which have better performance on Docker Desktop. - How do containers communicate in Docker Compose?
- Compose creates a default bridge network. Each service is reachable by its service name as a DNS hostname. The
webservice connects todb:5432— Docker's DNS resolvesdbto the container's IP automatically. - How do I convert a docker run command to Compose?
- Map each flag:
-p→ports,-v→volumes,-e→environment,--name→ service key. Use the Docker Run to Compose Converter to automate this. - How do I make Compose wait for a database to be ready?
- Add a
healthcheckto the database service and usedepends_on: db: condition: service_healthy. The basicdepends_onwithout a condition only waits for the container to start, not for the application to be ready.