Introduction

So far, we’ve dealt with single containers. But real-world applications are rarely that simple. They are often composed of multiple, interconnected services: a web server, a database, a caching layer, a message queue, and more. Managing the lifecycle, networking, and data for all these separate containers with individual docker run commands would be a nightmare.

This is the problem that Docker Compose solves. It is an essential tool for defining and running multi-container Docker applications. With a single, declarative YAML file, you can model your entire application stack and manage it with a few simple commands.

We will demonstrate its power by setting up a classic web application stack: a WordPress frontend that connects to a MySQL database.

Why is Docker Compose a Game-Changer?

Before Compose, you would need multiple, complex docker run commands:

# Before Compose: The Hard Way
# 1. Create a network first
docker network create my-app-net

# 2. Start the database container on the network
docker run -d --name db --network my-app-net \
  -v db_data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=wordpress \
  mysql:5.7

# 3. Start the web server container on the network
docker run -d --name web --network my-app-net \
  -p 80:80 \
  -e WORDPRESS_DB_HOST=db:3306 \
  -e WORDPRESS_DB_PASSWORD=secret \
  wordpress

This is manual, error-prone, and hard to share with your team.

With Docker Compose, this entire stack is defined in one file, docker-compose.yml, and managed with one command: docker-compose up.

Key Benefits:

  • Declarative Stack Definition: Your entire application environment is defined as code, making it reproducible and version-controllable.
  • Simplicity and Efficiency: Spin up or tear down your entire multi-service application with a single command.
  • Automated Service Discovery: Compose automatically creates a network for your services, allowing them to discover and communicate with each other using their service names as hostnames.
  • Environment Consistency: It solves the “it works on my machine” problem by ensuring that every developer runs the exact same environment.

Core Concepts of Docker Compose

Before diving into the YAML file, let’s understand the key concepts:

  • Services: A service is a definition of how to run a container. It includes the image to use, ports to expose, volumes to mount, environment variables, and more. Each service typically represents one component of your application (e.g., a web server, a database).
  • Networks: Compose automatically creates a default network for all services defined in the file. Services on the same network can communicate using their service names as hostnames.
  • Volumes: Top-level volumes can be defined and then attached to services, making data management clean and explicit.

Part 1: The docker-compose.yml File - A Deep Dive

The heart of Docker Compose is the docker-compose.yml file. Let’s create one to define our WordPress and MySQL services.

Create a new directory for your project:

mkdir my-wordpress-app
cd my-wordpress-app

Create a file named docker-compose.yml with the following content:

version: "3.8"

services:
  mydatabase:
    image: mysql:5.7
    restart: always
    volumes: 
      - mydata:/var/lib/mysql
    environment: 
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
    networks:
      - mynet

  mywordpress:
    image: wordpress:latest
    depends_on: 
      - mydatabase
    restart: always
    ports:
      - "80:80"
    environment: 
      WORDPRESS_DB_HOST: mydatabase:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress
    networks:
      - mynet

volumes:
  mydata: {}

networks:
  mynet:
    driver: bridge

Part 2: Understanding the Compose File Structure

Let’s break down this file section by section.

Top-Level Keys

  • version: "3.8": This specifies the Compose file format version. While it’s somewhat legacy (newer versions of Compose don’t strictly require it), it’s still good practice to include it for compatibility.

The services Section

This is where you define each component of your application.

Service: mydatabase

  • image: mysql:5.7: This tells Compose to use the official MySQL image, version 5.7. Pinning to a specific version (rather than using latest) is a best practice for reproducibility.
  • restart: always: This is a restart policy. If the container stops for any reason (crash, manual stop), Docker will automatically restart it. Other options include no, on-failure, and unless-stopped.
  • volumes: - mydata:/var/lib/mysql: This mounts the named volume mydata (defined at the bottom of the file) to /var/lib/mysql inside the container. This is where MySQL stores its database files, so this ensures data persistence.
  • environment:: This section sets environment variables inside the container. MySQL uses these to configure the database on first startup.
    • MYSQL_ROOT_PASSWORD: The password for the MySQL root user.
    • MYSQL_DATABASE: The name of the database to create.
    • MYSQL_USER and MYSQL_PASSWORD: A non-root user and password for the application to use.
    • Security Note: In production, never hardcode secrets in your docker-compose.yml. Use environment variable files (.env) or secret management tools.
  • networks: - mynet: This attaches the service to the custom network mynet.

Service: mywordpress

  • image: wordpress:latest: Uses the official WordPress image.
  • depends_on: - mydatabase: This is crucial. It tells Compose to start the mydatabase service before starting mywordpress. Important Caveat: depends_on only controls the startup order, not readiness. It doesn’t wait for MySQL to be fully initialized and ready to accept connections. For production, you’d use a tool like wait-for-it or implement retry logic in your application.
  • restart: always: Same restart policy as the database.
  • ports: - "80:80": This maps port 80 on the host to port 80 in the container, making the WordPress site accessible at http://localhost.
  • environment:: Configuration for WordPress.
    • WORDPRESS_DB_HOST: mydatabase:3306: This is the magic of service discovery. WordPress can connect to the database using the service name mydatabase as a hostname. Compose’s internal DNS resolves this to the database container’s IP address.
    • The other variables provide the database credentials.
  • networks: - mynet: Attaches to the same network as the database, enabling communication.

Top-Level volumes and networks

  • volumes: mydata: {}: This defines a named volume called mydata. The empty {} means it uses default settings. Docker will manage this volume’s storage location.
  • networks: mynet: driver: bridge: This defines a custom network named mynet using the bridge driver (the default for single-host networking).

Part 3: Running the Application with Docker Compose

With the docker-compose.yml file in your current directory, you can now manage the entire application stack with simple commands.

Step 1: Start the Application

Run the following command:

docker-compose up

This command will:

  1. Pull the mysql:5.7 and wordpress:latest images if they’re not already on your system.
  2. Create the mydata volume and the mynet network.
  3. Start the mydatabase container.
  4. Start the mywordpress container.
  5. Attach your terminal to the aggregated logs of both services.

You’ll see the logs from both MySQL and WordPress streaming in your terminal.

To run in detached mode (background):

docker-compose up -d

Step 2: Check the Status

You can see your running services with:

docker-compose ps

This will show the status of the mywordpress and mydatabase containers, including their ports and state.

Step 3: Access WordPress

Open your web browser and navigate to http://localhost. You should see the WordPress installation screen. The web application is successfully communicating with the database, all orchestrated by Docker Compose!

Step 4: View Logs

To view logs from all services:

docker-compose logs

To follow logs in real-time:

docker-compose logs -f

To view logs for a specific service:

docker-compose logs -f mywordpress

Step 5: Execute Commands in a Service

To get a shell inside the WordPress container:

docker-compose exec mywordpress bash

To run a one-off command:

docker-compose exec mydatabase mysql -u root -p

Step 6: Stop the Application

To stop the services without removing them:

docker-compose stop

To start them again:

docker-compose start

Step 7: Clean Up

To stop and remove all the containers and networks created by docker-compose up:

docker-compose down

Important: This does not remove the named volumes by default. Your database data in mydata will persist.

To also remove the volumes:

docker-compose down -v

Part 4: Building Custom Images with Compose

So far, we’ve used pre-built images from Docker Hub. But you can also build custom images as part of your Compose workflow.

Instead of the image key, use the build key:

services:
  webapp:
    build: .  # Path to the directory containing the Dockerfile
    ports:
      - "8000:8000"

When you run docker-compose up, Compose will automatically build the image from the Dockerfile in the current directory before starting the container.

Conclusion

Docker Compose is an indispensable tool for local development and for defining multi-service applications. It transforms the complexity of managing multiple containers into a simple, declarative workflow.

In this comprehensive guide, you learned:

  • The problems Docker Compose solves and why it’s essential.
  • The core concepts: services, networks, and volumes.
  • How to write a detailed docker-compose.yml file with a real-world example.
  • The complete workflow: starting, managing, debugging, and cleaning up multi-container applications.
  • How to build custom images as part of your Compose setup.

Docker Compose is your stepping stone to more advanced orchestration tools like Docker Swarm and Kubernetes, but for local development and small-scale deployments, it’s often all you need.