A docker-compose.yml file replaces a dozen docker run flags with one declarative file that starts an entire stack — web, database, cache — with a single command: docker compose up -d. Instead of memorizing port mappings, volume mounts, and network flags, the file describes each service, its image, ports, environment, and dependencies in versioned YAML.
This guide explains how to create a docker-compose.yml file from zero, the anatomy of services, volumes, and networks, step-by-step creation, three copy-paste examples, and the pitfalls that cause compose failures.
docker-compose.yml with a services: block — define each service with image or build, ports: ["host:container"] (quoted), env_file: .env, and volumes for persistence — then run docker compose up -d. Use a docker-compose generator to build the YAML with examples and no syntax memorization.
What Is docker-compose.yml?
docker-compose.yml is a YAML file that defines how Docker Compose should run multiple containers together. Per the Compose Specification, it declares services (containers), volumes (persistent data), and networks (connectivity) in one versioned file that lives alongside the code and is committed to git.
Where a Dockerfile builds one image, a compose file orchestrates many images. Where docker run is imperative (flags typed each time), compose is declarative (desired state in a file). This makes the stack reproducible: docker compose up -d recreates the same network, volumes, and containers on any machine.
Anatomy of a docker-compose.yml File
Answer-first: 3 top-level keys cover most files — services, volumes, and optional networks. Each service has image/build, ports, env_file, volumes, depends_on.
services:
web:
image: myapp:1.0 # or build: .
ports: ["3000:3000"] # host:container (quoted!)
env_file: .env # env vars from file
depends_on: [db] # start order
db:
image: postgres:16-alpine
volumes: [db-data:/var/lib/postgresql/data]
environment: {POSTGRES_DB: mydb}
volumes:
db-data: {} # named volume declared once
Key sections:
- services → each container:
web, db, redis— each withimage(pull) orbuild: .(build from Dockerfile),ports,env_file,environment,volumes,depends_on - volumes → persistent data: Named volumes like
db-datasurvivedocker compose down(removed only with-v). They are declared top-level once and referenced in services. - networks → connectivity: Omitted in minimal files — Compose creates a default bridge where services reach each other at
http://service-name:port(e.g., web → db atdb:5432). Custom networks are added only when isolation is needed.
YAML rules: Indent with 2 spaces (not tabs), quote "host:container" because of the colon, and note that depends_on controls start order, not readiness — the app must retry or use a healthcheck.
How to Create a docker-compose.yml File — 4 Steps
- Define services: Each service is one container. For a web + db + cache stack, define
web: image: myapp:1.0,db: image: postgres:16-alpine, andcache: image: redis:alpine. Usebuild: .if a Dockerfile should be built, orimage: nginxto pull. - Map ports and volumes:
ports: ["3000:3000"]publishes the container port to the host (quoted).volumes: [db-data:/var/lib/postgresql/data]mounts a named volume for persistence. Without a volume, database data is lost ondown. - Set environment: Never hardcode secrets in the YAML. Use
env_file: .env(with.envin.gitignore) orenvironment: {POSTGRES_DB: mydb}for non-sensitive config. Compose loads.envautomatically if present. - Bring it up:
docker compose up -dbuilds (ifbuild:), creates the default network and volumes, and starts all services.docker compose psshows status,docker compose logs -ffollows logs, anddocker compose downstops and removes the network. Add-vto also remove volumes (caution: deletes data).
Minimal working file for a Node + Postgres app:
services:
web:
image: myapp:1.0
ports: ["3000:3000"]
env_file: .env
db:
image: postgres:16-alpine
volumes: [db-data:/var/lib/postgresql/data]
volumes: { db-data: {} }
docker-compose.yml Examples — Copy-Paste Ready
Full-Stack (Node + Postgres)
services:
web:
build: .
ports: ["3000:3000"]
env_file: .env
depends_on: [db]
db:
image: postgres:16-alpine
volumes: [pgdata:/var/lib/postgresql/data]
environment: {POSTGRES_DB: mydb, POSTGRES_USER: user, POSTGRES_PASSWORD: pass}
volumes: { pgdata: {} }
Web at http://localhost:3000, DB at db:5432 from the web container.
WordPress (WP + MySQL)
services:
wp:
image: wordpress:latest
ports: ["8080:80"]
environment: {WORDPRESS_DB_HOST: db, WORDPRESS_DB_USER: wpuser, WORDPRESS_DB_PASSWORD: wppass}
db:
image: mysql:8
environment: {MYSQL_ROOT_PASSWORD: secret, MYSQL_DATABASE: wordpress}
volumes: [dbdata:/var/lib/mysql]
volumes: { dbdata: {} }
WP at http://localhost:8080 — classic CMS stack in one file.
App + Cache (Node + Redis)
services:
app:
image: myapp:1.0
ports: ["3000:3000"]
cache:
image: redis:alpine
ports: ["6379:6379"]
volumes: [redis:/data]
volumes: { redis: {} }
Add caching in 5 lines — app reaches cache at cache:6379.
Common Mistakes to Avoid
- Unquoted ports:
ports: 3000:3000fails — YAML parses3000:3000as a number. Always quote:ports: ["3000:3000"]. - Hardcoded secrets:
environment: POSTGRES_PASSWORD: mysecretgets committed. Useenv_file: .envand add.envto.gitignore. - Assuming
depends_onwaits: It startsdbfirst, but the app may connect before the DB is ready. Fix with retry logic in the app or a healthcheck pluscondition: service_healthy. - Using
version: '3.8': The Compose Spec no longer requires it — it is ignored. The generator omits it. - Misunderstanding
down -v:docker compose downkeeps volumes;down -vdeletes them. The latter is destructive for databases.
Compose vs Dockerfile vs docker run — When to Use Which
| Tool | Scope | Use |
|---|---|---|
| Dockerfile | One image | Build a single container type |
| docker-compose.yml | Many images | Orchestrate web + db + cache with one command |
docker run | One container | Manual, imperative flags for single runs |
Compose files are declarative and versioned — commit them to git. docker run flags are imperative and easy to mistype.
Using a Generator (No YAML to Memorize)
Select services (web, db, cache) with pre-filled images (postgres:16-alpine, redis:alpine), set ports and env_file, and copy the generated YAML. The generator ensures 2-space indentation, quoted ports, correct nesting, and valid Compose Spec — no YAML memorization. Change services to adapt across stacks; the pattern remains the same.
FAQs About docker-compose.yml
What is docker-compose.yml?
A YAML file that defines services, volumes, and networks for Docker Compose. It lets multiple containers be started with docker compose up -d instead of many docker run commands.
How do I run a docker-compose.yml file?
Place the file in the project root and run docker compose up -d to start, docker compose ps for status, docker compose logs -f for logs, and docker compose down to stop.
What is the difference between Dockerfile and docker-compose.yml?
A Dockerfile builds one image; a compose file orchestrates multiple images (services) plus their ports, volumes, and networks. The Dockerfile defines a container type; compose defines the stack.
Do I need to create networks and volumes manually?
No — Compose creates a default bridge network and named volumes automatically when declared. Services reach each other at http://service-name:port (e.g., db:5432).
How do I use environment variables in compose?
Use env_file: .env to load from a file (add .env to .gitignore) or environment: {KEY: value} for non-sensitive config. Never commit secrets in the YAML.
Why is my compose file failing to parse?
Common causes are unquoted ports ("3000:3000" required), tabs instead of 2-space indentation, and a missing services: key. A generator produces valid YAML that avoids these.
Conclusion
A docker-compose.yml file turns a fragile set of docker run flags into a reproducible, versioned stack. Define services, map ports and volumes, load environment from .env, and bring the whole app up with one command. When the next service is added, the pattern repeats without relearning flags.
Generate the next compose file with a visual builder — select services, set ports and env, and copy a correct YAML file in seconds.