If you’ve run more than one container by hand, you already know the pain. A web app needs a
database, the database needs a volume so its data survives a restart, both need to share a
network, and each one carries a docker run line long enough to wrap three times in your
terminal. Get a flag wrong and you start over.
Docker Compose fixes that. You write one YAML file that describes every container, its
network, its storage, and its settings. Then a single command brings the whole stack up,
and another tears it back down. No more hunting through shell history for the exact docker run you used last week.
This guide gets you from zero to a running two-container app: WordPress talking to MySQL, defined in one file. Along the way you’ll learn what each part of a Compose file does and the handful of commands you’ll use every day.
What Docker Compose actually does
Compose reads a declarative file. You describe the end state you want, and Compose makes the running system match it. That’s the whole idea.
Compare the two approaches. Running WordPress and MySQL by hand looks like this:
docker network create wp-net
docker volume create db-data
docker run -d --name db --network wp-net \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=wordpress \
-v db-data:/var/lib/mysql mysql:8.0
docker run -d --name wp --network wp-net \
-e WORDPRESS_DB_HOST=db \
-e WORDPRESS_DB_PASSWORD=secret \
-p 8080:80 wordpress:latest
Four commands, careful ordering, and you have to remember every flag next time. With Compose,
all of that lives in a file you can read, edit, and commit to git. One docker compose up
recreates the same setup byte for byte, on your laptop or on a server.
Check that Compose is installed
Modern Docker ships Compose as a built-in plugin. You call it with docker compose — two
words, a space between them. Confirm it’s there:
docker compose version
A healthy result prints something like Docker Compose version v2.x.x. If that works, you’re
ready and don’t need to install anything else.
You may also have an older command called docker-compose (one word, a hyphen). That’s the
original standalone tool, written in Python, and it’s now deprecated. The two behave almost
identically for basic files, but the plugin is what’s maintained going forward.
Compose v2 plugin vs the old standalone tool
| docker compose (space) | Current v2 plugin, bundled with Docker. Use this. |
|---|---|
| docker-compose (hyphen) | Legacy v1 Python tool. Deprecated, but still found on older hosts. |
| compose.yaml | Preferred filename in the Compose spec; picked up automatically. |
| docker-compose.yml | Older default name; still fully supported. |
How a Compose file is structured
A Compose file is plain YAML, so indentation matters — two spaces per level, no tabs. It has a few top-level keys, and you’ll use three of them constantly:
services:— the containers you want to run. Each service is named (likedborweb) and points at an image or a build context.volumes:— named storage that lives outside the container, so data survives a rebuild.networks:— optional. Compose creates a default network for you, so you only define this when you need something custom.
Inside a service, the keys you’ll reach for most are image, ports, volumes,
environment, and depends_on. Here’s what each one means in plain terms:
Core service directives
| image | Which image to run, e.g. mysql:8.0. Pin a real tag, not latest, for predictable behaviour. |
|---|---|
| build | Build from a local Dockerfile instead of pulling an image. Use this for your own app code. |
| ports | Map host:container, e.g. "8080:80". Left side is the port on your machine. |
| volumes | Attach storage. name:/path for a named volume, ./local:/path to mount a host folder. |
| environment | Set env vars inside the container — passwords, hostnames, config. |
| depends_on | Start order. The web service waits for the db service to be created first. |
| restart | Restart policy, e.g. unless-stopped, so containers come back after a reboot. |
One detail that trips up beginners: inside a Compose network, services reach each other by
their service name. The WordPress container connects to the database using the hostname
db, because that’s what the service is called. There’s no IP address to look up and no
--link flag. Compose wires up that internal DNS for you.
flowchart LR U["Your browser"] -->|localhost:8080| W["web service<br/>(WordPress)"] W -->|hostname: db| D["db service<br/>(MySQL)"] D -->|reads/writes| V["named volume<br/>db-data"] subgraph net["Compose network (auto-created)"] W D end
A working example: WordPress + MySQL
Time to build something real. Create an empty folder, drop in the file below as
compose.yaml, and you’ll have a complete WordPress site backed by MySQL.
services:
db:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: change-this-root-pw
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: change-this-user-pw
volumes:
- db-data:/var/lib/mysql
web:
image: wordpress:latest
restart: unless-stopped
depends_on:
- db
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: change-this-user-pw
WORDPRESS_DB_NAME: wordpress
volumes:
- wp-content:/var/www/html/wp-content
volumes:
db-data:
wp-content:
Read it top to bottom and it tells a story. There are two services. db runs MySQL 8.0,
stores its files in a named volume called db-data, and gets its root password and initial
database from environment variables. web runs WordPress, waits for db, publishes port 80
inside the container as 8080 on your host, and points its WORDPRESS_DB_HOST at db — the
service name. The volumes: block at the bottom declares the two named volumes so Docker
manages them.
Start it:
docker compose up -d
The -d flag runs everything detached, in the background. Compose pulls both images, creates
the network and volumes, and starts the containers in dependency order. Give it a minute on
the first run while MySQL initializes, then open http://localhost:8080 and you’ll land on
the WordPress installer.
The commands you’ll use every day
Run all of these from the folder that holds your compose.yaml. Compose finds the file
automatically.
# Start the whole stack in the background
docker compose up -d
# See what's running, with status and ports
docker compose ps
# Follow logs from every service (Ctrl+C to stop watching)
docker compose logs -f
# Follow logs from one service only
docker compose logs -f web
# Restart a single service after a config change
docker compose restart web
# Rebuild images if you use a build: section
docker compose build
# Stop and remove containers + the default network
docker compose down
That’s most of the job right there. You start with up -d, check health with ps, read
logs when something misbehaves, and down when you’re finished. Here’s the quick reference:
Everyday Compose commands
| docker compose up -d | Create and start everything in the background |
|---|---|
| docker compose down | Stop and remove containers and the network (volumes kept) |
| docker compose ps | List the stack's containers and their state |
| docker compose logs -f | Stream logs from all services live |
| docker compose restart | Restart services without recreating them |
| docker compose build | Build or rebuild images defined with build: |
| docker compose pull | Pull newer versions of the images you reference |
| docker compose stop | Stop containers but leave them in place to start later |
down vs down -v: the one that bites people
This is the part to read twice. docker compose down and docker compose down -v look almost
identical, and the difference is your data.
Plain down stops the containers and removes them along with the network Compose created. Your
named volumes stay. So when you run up -d again, MySQL finds its db-data volume right
where it left it, and your WordPress posts are still there.
Add -v and Compose also deletes the named volumes listed in your file. For the example above,
that means db-data and wp-content are gone — the entire database and all uploaded media,
erased. There’s no recycle bin and no undo.
A safe habit: use docker compose stop when you just want to pause work, plain down when you
want to clear out containers but keep data, and reserve down -v for throwaway test stacks you
genuinely want to reset.
Before you run down -v
- Confirm which volumes are named in the volumes: block of your file
- Check whether any of them hold a database or user uploads
- Back up the volume first if the data matters (docker run with a tar mount, or a db dump)
- Make sure you actually want a clean slate, not just a restart
- For a normal stop, use plain 'down' or 'stop' instead
Making changes the right way
Once a stack is running, Compose makes edits painless. Change something in the file — bump an image tag, add an environment variable, expose another port — then run:
docker compose up -d
Compose compares the file to what’s actually running and recreates only the containers that changed. Everything else keeps running untouched, and your named volumes survive, so a config tweak or an image update doesn’t cost you data.
To pull newer images before recreating:
docker compose pull
docker compose up -d
If your project builds a custom image from a Dockerfile (using a build: section instead of
image:), force a fresh build with:
docker compose up -d --build
Where to go next
You now have the core of Docker Compose: one YAML file describing services, volumes, and networking, brought up and down with a couple of commands. The WordPress and MySQL example is a real pattern you’ll reuse constantly — swap WordPress for any app and MySQL for whatever backing store it needs, and the shape stays the same.
A few things worth learning once you’re comfortable: moving secrets into a .env file,
adding healthcheck blocks so depends_on waits for a service to be ready rather than just
started, and putting a reverse proxy in front of your web service so you can run several sites
behind one set of ports. Each builds directly on what you’ve done here.
For more container walkthroughs and fixes, browse the Docker & Containers guides
or the full article library. And remember the one rule that saves data: plain
down keeps your volumes, down -v destroys them.