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 usinglatest
) 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 includeno
,on-failure
, andunless-stopped
.volumes: - mydata:/var/lib/mysql
: This mounts the named volumemydata
(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
andMYSQL_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 networkmynet
.
Service: mywordpress
image: wordpress:latest
: Uses the official WordPress image.depends_on: - mydatabase
: This is crucial. It tells Compose to start themydatabase
service before startingmywordpress
. 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 likewait-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 athttp://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 namemydatabase
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 calledmydata
. The empty{}
means it uses default settings. Docker will manage this volume’s storage location.networks: mynet: driver: bridge
: This defines a custom network namedmynet
using thebridge
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:
- Pull the
mysql:5.7
andwordpress:latest
images if they’re not already on your system. - Create the
mydata
volume and themynet
network. - Start the
mydatabase
container. - Start the
mywordpress
container. - 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.