New Customers: 50% OFF Your First Month on All VPS Servers & Web Hosting Plans!

Docker Compose, and why you want it

Docker Compose lets you define multi-container applications in a single YAML file and manage them with simple commands. This guide covers services, volumes, networks, environment files and a real WordPress plus MySQL example.

Rhys CallowayLinux VPS, servers, security and the command line 9 min read Updated 23 Sep 2026 AlmaLinux 9, Ubuntu 24.04

Docker Compose, and why you want it

Docker Compose lets you define your application in one YAML file and bring it up with a single command. It replaces long docker run lines, manages services, networks and volumes together, and gives you one project to start, stop, update and remove. The result is fewer mistakes, quicker repeats, and cleaner upgrades.

This picks up from a server you can already reach over SSH.

Before you start

  • Use the modern CLI: docker compose (with a space). The old docker-compose (with a hyphen) was the v1 tool and is deprecated. Compose v2 is built into the Docker CLI.
  • Compose discovers your file automatically when you name it compose.yaml or docker-compose.yaml and keep it in the project directory. It walks up the directory tree from your working directory to find it.
  • Project scoping matters. Compose uses the directory name as the default project name. It prefixes containers, networks and volumes with this name to avoid clashes. You can override with -p or the COMPOSE_PROJECT_NAME environment variable.
  • Understand the two kinds of environment files:
    • .env in the project directory is for variable interpolation in compose.yaml. It is not injected into containers.
    • env_file under a service injects key=value pairs into that container at runtime. It does not affect interpolation in compose.yaml.
  • Mind the lifecycle commands:
    • docker compose up creates or updates containers, networks and any declared named volumes.
    • docker compose stop stops containers without removing them.
    • docker compose down removes containers and networks. Add -v to remove volumes as well. This deletes data.
  • Data loss risks:
    • docker compose down -v removes named volumes declared in the file and anonymous volumes. Databases and other state will be lost.
    • docker volume prune and docker system prune --volumes remove unused volumes. A volume is unused if no container currently references it, which includes stacks that are stopped but not removed.
    • Mounting a volume or bind mount over a non empty path hides what is in the image at that path. An empty named volume may be initialised from the container path on first use. A bind mount overlays the host directory and obscures container content.
  • Validate before you apply. docker compose config resolves variables and merges files. It shows exactly what Docker will run.

Step 1: Install the Docker Compose plugin

Install the Docker Compose v2 plugin so you can use the docker compose command.

Ubuntu 24.04

This updates the package list, then installs the Compose plugin from the package repository.

sudo apt update
sudo apt install -y docker-compose-plugin

AlmaLinux 9

This installs the Compose plugin from your configured repositories.

sudo yum install -y docker-compose-plugin

Verify the installation to confirm Docker sees the Compose plugin.

docker compose version

Step 2: Create a project directory and choose a project name

Keep your Compose file in a single project directory so discovery, scoping and relative paths behave as you expect. Compose will derive the project name from this directory, which scopes container, network and volume names.

sudo mkdir -p /opt/wordpress
cd /opt/wordpress

If you need a different project name than the directory, you can override it when you run a command.

docker compose -p wpdemo ps

You can also set COMPOSE_PROJECT_NAME in your shell environment before running compose commands.

Step 3: Decide what belongs in .env versus service env files

  • Put values you want to substitute into compose.yaml in .env (for example, image tags and host port numbers). Compose auto loads .env from the project directory for interpolation.
  • Put container runtime settings like database names and passwords in a separate env_file per service, then reference those files under that service. These values are injected into the container environment when it runs.

This split keeps secrets out of the Compose model when they do not need to be there, and makes it clear which values control the model versus the container environment. Do not commit secrets to a public repository.

Step 4: Create the environment files

Create a .env file in /opt/wordpress for interpolation in compose.yaml.

# /opt/wordpress/.env
WP_PORT=8080
WORDPRESS_TAG=latest
MYSQL_TAG=8.0

Create a db.env file for the MySQL container. These values configure the database. Change the passwords.

# /opt/wordpress/db.env
MYSQL_DATABASE=wordpress
MYSQL_USER=wordpress
MYSQL_PASSWORD=change-me
MYSQL_ROOT_PASSWORD=change-me-root

Create a wordpress.env file for the WordPress container to tell it how to reach the database.

# /opt/wordpress/wordpress.env
WORDPRESS_DB_HOST=db:3306
WORDPRESS_DB_USER=wordpress
WORDPRESS_DB_PASSWORD=change-me
WORDPRESS_DB_NAME=wordpress

Step 5: Write compose.yaml for a real stack (WordPress + MySQL)

This Compose file defines two services, two named volumes for persistence, and a port mapping to expose WordPress on the host. It relies on the default project network, which lets services talk to each other by service name via built in DNS. You do not need to declare a network for this basic case.

# /opt/wordpress/compose.yaml
services:
  db:
    image: mysql:${MYSQL_TAG}
    env_file:
      - db.env
    volumes:
      - db-data:/var/lib/mysql

  wordpress:
    image: wordpress:${WORDPRESS_TAG}
    depends_on:
      - db  # orders start, not readiness
    ports:
      - "${WP_PORT}:80"
    env_file:
      - wordpress.env
    volumes:
      - wp-data:/var/www/html

volumes:
  db-data:
  wp-data:

Notes:

  • services is required. Each service describes the image, ports, environment and mounts it needs.
  • volumes at the top level declares named volumes. Compose creates any missing named volumes on the first up. The names are scoped by the project name to avoid clashes.
  • The default network will be <project>_default. Both services join it automatically and can reach each other by service name. For example, WordPress reaches MySQL at db:3306.
  • If you have an existing data directory you want to mount from the host, use a bind mount. Take care with paths. The short syntax creates the host directory if it does not exist, which can hide files inside the container path and trigger a first run initialisation.

Step 6: Validate the configuration

Use docker compose config to render the final model. This resolves variables from .env and merges any additional files you provide with -f. It helps catch wrong paths and unexpected values before you affect a live stack.

docker compose config

If you use multiple -f files, remember that all relative paths resolve from the first file unless you change it with --project-directory.

Step 7: Start the stack

Start your project in detached mode. This pulls images if needed, creates the named volumes, creates the default network and starts the containers.

docker compose up -d

Check container status and view logs.

docker compose ps
docker compose logs -f wordpress

Open your browser to http://<your-server-ip>:8080 to complete the WordPress setup.

Step 8: Manage lifecycle safely

  • Update configuration, then apply changes:
    docker compose up -d
    
    This replaces changed containers and reconnects them to the same project networks under the same DNS names.
  • Stop services without removing anything:
    docker compose stop
    
  • Remove containers and the project network:
    docker compose down
    
  • Remove containers, the project network and all named and anonymous volumes used by the services:
    docker compose down -v
    
    This deletes database and application data. Confirm backups before running with -v.
  • Clean up unused volumes across the host. Use with care because volumes from stopped stacks count as unused:
    docker volume prune
    # or include unused named volumes too
    docker system prune --volumes
    

Step 9: Understand volumes and mounts

  • A named volume mounted onto a non empty path in the image hides that image content. On first use, Docker may initialise an empty named volume with the content from the container path.
  • A bind mount overlays the host directory on the container path. It always hides image content under that path.
  • The short bind mount syntax auto creates the host directory if it is missing. A typo in a path can give you an empty directory and make an application behave like a fresh install. Double check absolute versus relative paths.

Example: bind mounting a local theme directory for development. This exposes your host’s wp-content directory to the container. Use absolute paths, and create the directory before mounting to avoid accidental empty overlays.

services:
  wordpress:
    # ...
    volumes:
      - wp-data:/var/www/html
      - /opt/wordpress/wp-content:/var/www/html/wp-content

Step 10: Use networks thoughtfully

Compose creates a single default network per project and connects all services to it. That is enough for most stacks. Services can discover each other by name using built in DNS. For segmentation, declare custom networks at the top level and attach services to the ones they need.

services:
  db:
    # ...
    networks:
      - back-tier

  wordpress:
    # ...
    networks:
      - front-tier
      - back-tier

networks:
  front-tier:
  back-tier:

To reuse an existing Docker network, mark it external. Compose will not create or delete external networks.

networks:
  shared-edge:
    external: true

Step 11: Handle startup order and readiness

depends_on only orders container start. It does not wait for a service to be ready. If WordPress starts before MySQL accepts connections, initial boot can fail. Add a healthcheck to the database service, then use the long form of depends_on with condition: service_healthy so WordPress waits for a healthy DB.

services:
  db:
    # ...
    healthcheck:
      # Use a command that returns 0 when the DB is ready to serve connections
      test: ["CMD", "your-readiness-command"]
      interval: 10s
      timeout: 5s
      retries: 5

  wordpress:
    # ...
    depends_on:
      db:
        condition: service_healthy

Replace your-readiness-command with a command appropriate for your image that exits with 0 when ready.

Step 12: Organise larger projects

  • Project names: Keep each stack in its own directory to get clean scoping. Override with -p when you need to run a second copy on the same host:
    docker compose -p wpdemo2 up -d
    
  • Multiple files: Layer configuration with -f files. All relative paths resolve from the first file unless you set --project-directory.
    docker compose -f compose.yaml -f compose.prod.yaml config
    docker compose -f compose.yaml -f compose.prod.yaml up -d
    
  • Alternate env files: Point Compose at a different interpolation file with --env-file when switching environments.
    docker compose --env-file .env.staging up -d
    
  • Include and reuse: For very large stacks, use the include top level element to modularise your Compose configuration. This keeps repeated service definitions in one place.
  • Versions and compatibility: Compose v2 and v5 use the same docker compose command. The old version: key from v1 projects is ignored by v2 and later.

Why Compose beats long docker run commands

  • One source of truth: services, networks and volumes expressed together in YAML.
  • Reproducible: docker compose up creates or updates the whole stack in one step, including named volumes and networks.
  • Scoped and tidy: automatic project scoping prevents name clashes, and docker compose down removes the stack cleanly.
  • Discoverable: short names, documented mounts and explicit ports are easier for teams to review than long shell lines.
  • Safer changes: validate with docker compose config, then apply. Rollouts replace containers and rejoin networks under the same DNS names.

What next

If you want a capable place to run this, see our Linux VPS in the UK and US. For more step by step VPS topics, browse our VPS guides.

If you get stuck on any step, open a support ticket and we will help.

Common questions

Is docker-compose still supported?

No. The old docker-compose v1 tool is deprecated. Use Compose v2 via the Docker CLI with the docker compose command. It is the supported path.

Where should I keep compose.yaml?

Put it in your project directory and name it compose.yaml or docker-compose.yaml. Compose will find it by walking up from your working directory. Keeping it in the project root also sets a useful default project name for scoping.

What belongs in .env versus env_file?

.env is for interpolation in compose.yaml, such as image tags and host ports. It is not injected into containers. env_file is referenced under a specific service and injects those key=value lines into that container at runtime. Do not assume one overrides the other. Check the precedence rules if values come from multiple places, then confirm with docker compose config.

Why does my app start before the database is ready?

Plain depends_on only orders container start. It does not wait for readiness. Add a healthcheck to the database service, then use the long form of depends_on with condition: service_healthy to make the application wait for a healthy DB.

Why did my data disappear after an update?

Changing a volume’s name or its target path in compose.yaml creates a new volume on the next up, which makes the service start with empty data. Bind mounting the wrong host path has a similar effect because it overlays the container path with an empty directory. Keep volume names stable, double check paths, and validate with docker compose config before applying changes.