Skip to content
Docker & Containers Pillar Guide

Docker Compose Beginner Guide: Run Multiple Containers with One File

Learn Docker Compose from scratch: write one YAML file to run multiple containers, with a working WordPress + MySQL example and the core up/down commands.

SDSysadmin Desk May 26, 2026 10 min read
Diagram-style cover showing one compose.yaml file defining a web container and a database container connected on a shared Docker network

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 (like db or web) 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.

How two services share a network
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
Compose puts every service on a shared network and resolves them by name, so 'web' reaches 'db' as a hostname.

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.

Frequently asked questions

What is the difference between Docker and Docker Compose?

Docker runs individual containers; you start each one with its own docker run command. Docker Compose sits on top of Docker and reads a single YAML file that describes several containers, their networks, and their volumes. One docker compose up command then starts the whole stack the same way every time.

Is it compose.yaml or docker-compose.yml?

Both work. The Compose specification prefers compose.yaml, and the modern docker compose command looks for it first. docker-compose.yml is the older default and is still fully supported, so you'll see it in most existing projects. Pick one name per project and stay consistent.

Do I need to install Docker Compose separately?

On current versions of Docker Desktop and Docker Engine, no. Compose ships as a built-in plugin you call with docker compose (a space, not a hyphen). The old standalone docker-compose binary is a separate Python tool that's now deprecated, though many systems still have it installed.

What does docker compose down -v actually delete?

down stops and removes the containers and the default network. Adding -v also removes named volumes declared in the volumes section of your file, which is where databases store their data. Running down -v on a stack with a database wipes that database. Use plain down unless you genuinely want a clean slate.

How do I update a container managed by Compose?

Edit the image tag or build context in your compose file, then run docker compose pull (for pulled images) and docker compose up -d. Compose compares the running state to the file, recreates only the containers that changed, and leaves named volumes intact so your data survives the update.

Sources & further reading

Official vendor documentation referenced while writing this guide.

SD

Sysadmin Desk

Infrastructure & Cloud

Hands-on guidance for infrastructure, virtualization, and containers — Hyper-V, VMware, Docker, and the day-to-day operations work that keeps environments running.

MCSA Guru provides independent, educational IT guidance. Microsoft, Windows, Windows Server, Microsoft 365, Exchange, and Microsoft Teams are trademarks of Microsoft Corporation; Docker is a trademark of Docker, Inc. MCSA Guru is not affiliated with or endorsed by Microsoft or Docker. Always test changes in a safe environment before applying them in production.

Related guides

Fixing something right now?

Jump straight into the guide library or search for the exact error or task you are dealing with.