From multiple
docker runcommands to one declarative YAML file.
A real application rarely consists of just one container.
You might have:
Web App
↓
Database
↓
Redis
↓
Message Queue
Enter fullscreen mode Exit fullscreen mode
Starting and configuring all of these containers manually can quickly become painful.
That’s where Docker Compose comes in.
In this article, we’ll learn how Docker Compose simplifies multi-container applications and how to define an entire application stack in a single YAML file.
Why Docker Compose Matters
Think back to a typical multi-container setup.
You might need commands like:
docker network create mongo-network
docker volume create mongo-data
docker run -d \
--name mongo \
--network mongo-network \
-v mongo-data:/data/db \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=password123 \
mongo
docker run -d \
--name mongo-express \
--network mongo-network \
-e ME_CONFIG_MONGODB_SERVER=mongo \
-e ME_CONFIG_MONGODB_ADMINUSERNAME=admin \
-e ME_CONFIG_MONGODB_ADMINPASSWORD=password123 \
-p 8081:8081 \
mongo-express
Enter fullscreen mode Exit fullscreen mode
It works.
But it’s:
- Long
- Difficult to remember
- Easy to mistype
- Hard to share
- Difficult to maintain
Now imagine having six or ten services.
You don’t want to manage your application this way.
Docker Compose lets you describe the entire application in a single file.
Then:
docker compose up
Enter fullscreen mode Exit fullscreen mode
And your application stack starts.
That’s the real power of Compose.
What Is Docker Compose?
Docker Compose is a tool for defining and running multi-container Docker applications.
Instead of manually running individual containers, you describe your desired application architecture in a YAML file.
For example:
services:
web:
build: .
database:
image: mongo
cache:
image: redis
Enter fullscreen mode Exit fullscreen mode
Compose reads this configuration and creates the required containers, networks, and volumes.
The important idea is:
Define your application once, then let Docker Compose create and manage it.
Imperative vs Declarative
Docker Compose introduces an important DevOps concept.
Imperative
With individual Docker commands, you tell Docker what to do.
docker network create app-network
docker volume create database-data
docker run ...
docker run ...
Enter fullscreen mode Exit fullscreen mode
You’re specifying the steps.
Declarative
With Compose, you describe what you want.
services:
web:
build: .
database:
image: mongo
Enter fullscreen mode Exit fullscreen mode
You’re saying:
“I want a web service and a database service.”
Compose determines how to create them.
This declarative approach becomes extremely important later when you learn:
- Kubernetes
- Terraform
- Infrastructure as Code
- CI/CD configuration
The docker-compose.yml File
Docker Compose uses YAML configuration.
A simple file looks like this:
services:
web:
image: nginx
database:
image: mongo
Enter fullscreen mode Exit fullscreen mode
The main section is:
services:
Enter fullscreen mode Exit fullscreen mode
Each service represents a container.
For example:
services:
web:
image: nginx
database:
image: mongo
cache:
image: redis
Enter fullscreen mode Exit fullscreen mode
This describes three services:
Docker Compose
|
+-------+-------+
| | |
Web Database Cache
nginx mongo redis
Enter fullscreen mode Exit fullscreen mode
YAML Indentation Matters
YAML uses indentation to represent structure.
Use spaces, not tabs.
For example:
services:
web:
image: nginx
ports:
- "8080:80"
Enter fullscreen mode Exit fullscreen mode
Incorrect indentation can cause YAML parsing errors.
Defining a Service
Let’s look at a realistic service:
services:
mongo:
image: mongo
container_name: mongo
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password123
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
Enter fullscreen mode Exit fullscreen mode
Most of these options map directly to docker run flags you’ve already learned.
image
image: mongo
Enter fullscreen mode Exit fullscreen mode
This tells Compose which Docker image to use.
It’s similar to:
docker run mongo
Enter fullscreen mode Exit fullscreen mode
You can also specify a version:
image: mongo:8
Enter fullscreen mode Exit fullscreen mode
Pinning versions is generally better than relying on a moving latest tag for production workloads.
container_name
container_name: mongo
Enter fullscreen mode Exit fullscreen mode
This gives the container a predictable name.
It’s similar to:
docker run --name mongo
Enter fullscreen mode Exit fullscreen mode
In Compose projects, you often don’t need container_name because Compose already provides predictable service-based naming and DNS.
environment
Environment variables can be defined like this:
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password123
Enter fullscreen mode Exit fullscreen mode
This is equivalent to:
-e MONGO_INITDB_ROOT_USERNAME=admin
-e MONGO_INITDB_ROOT_PASSWORD=password123
Enter fullscreen mode Exit fullscreen mode
For real applications, avoid committing production secrets directly into your Compose file. Environment files or secret-management solutions are better choices.
ports
Port mappings use the same familiar syntax:
ports:
- "8080:80"
Enter fullscreen mode Exit fullscreen mode
This means:
Host Container
8080 ---> 80
Enter fullscreen mode Exit fullscreen mode
It’s the Compose equivalent of:
-p 8080:80
Enter fullscreen mode Exit fullscreen mode
volumes
Volumes provide persistent storage:
volumes:
- mongo-data:/data/db
Enter fullscreen mode Exit fullscreen mode
This connects the named volume mongo-data to /data/db inside the MongoDB container.
At the bottom of the Compose file, declare the volume:
volumes:
mongo-data:
Enter fullscreen mode Exit fullscreen mode
build — Building Your Own Application
For your own application, use build: instead of image:.
Suppose your project looks like this:
my-app/
├── Dockerfile
├── app.py
├── requirements.txt
└── docker-compose.yml
Enter fullscreen mode Exit fullscreen mode
Your Compose file can contain:
services:
web:
build: .
ports:
- "5000:5000"
Enter fullscreen mode Exit fullscreen mode
The:
build: .
Enter fullscreen mode Exit fullscreen mode
means:
Build the image using the Dockerfile in the current directory.
depends_on
Suppose your web application needs MongoDB.
You can write:
services:
web:
build: .
depends_on:
- mongo
mongo:
image: mongo
Enter fullscreen mode Exit fullscreen mode
This tells Compose to start MongoDB before starting the web container.
But remember:
depends_oncontrols startup order, not application readiness.
A database can still be initializing after its container has started.
For more reliable startup behavior, healthcheck can be used.
Docker Compose Networking
One of Docker Compose’s biggest advantages is automatic networking.
When you start a Compose project, Compose creates a network for the application by default.
Services can communicate with each other using their service names.
For example:
services:
web:
build: .
mongo:
image: mongo
Enter fullscreen mode Exit fullscreen mode
The web container can connect to MongoDB using:
mongo
Enter fullscreen mode Exit fullscreen mode
as the hostname.
You don’t need to know MongoDB’s container IP address.
Conceptually:
+-----------------------------+
| Compose Network |
| |
| +--------+ +---------+ |
| | web |-->| mongo | |
| +--------+ +---------+ |
| |
+-----------------------------+
Enter fullscreen mode Exit fullscreen mode
Defining a Custom Network
You can also explicitly define your network:
services:
web:
build: .
networks:
- app-network
mongo:
image: mongo
networks:
- app-network
networks:
app-network:
Enter fullscreen mode Exit fullscreen mode
Now both services belong to app-network and can communicate with each other.
Restart Policies
Compose makes it easy to define what should happen when a container stops.
restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode
Common policies include:
Policy Behaviorno
Never automatically restart
always
Always restart
unless-stopped
Restart unless manually stopped
on-failure
Restart when the process exits with an error
For many long-running services:
restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode
is a useful default.
The Most Important Compose Commands
Start the Application
docker compose up
Enter fullscreen mode Exit fullscreen mode
Run in the Background
docker compose up -d
Enter fullscreen mode Exit fullscreen mode
Build Before Starting
docker compose up --build
Enter fullscreen mode Exit fullscreen mode
Check Service Status
docker compose ps
Enter fullscreen mode Exit fullscreen mode
View Logs
docker compose logs
Enter fullscreen mode Exit fullscreen mode
View Logs for One Service
docker compose logs mongo
Enter fullscreen mode Exit fullscreen mode
Follow Logs
docker compose logs -f mongo
Enter fullscreen mode Exit fullscreen mode
docker compose stop vs docker compose down
This distinction is extremely important.
docker compose stop
docker compose stop
Enter fullscreen mode Exit fullscreen mode
Stops the containers but doesn’t remove them.
You can start them again with:
docker compose start
Enter fullscreen mode Exit fullscreen mode
docker compose down
docker compose down
Enter fullscreen mode Exit fullscreen mode
Stops and removes the containers and the Compose-created network.
By default, named volumes are preserved.
That means your database data can remain intact.
Be Careful With down -v
You can also run:
docker compose down -v
Enter fullscreen mode Exit fullscreen mode
The -v option removes the Compose-managed volumes.
If your database uses those volumes, your database data can be deleted.
Use this command carefully, especially on production systems.
A Complete Real-World Example
Imagine we have:
- Flask application
- MongoDB
- Mongo Express
- Persistent MongoDB storage
- Custom application network
- Restart policies
Our Compose file can look like this:
services:
web:
build: .
container_name: flask-app
restart: unless-stopped
ports:
- "5000:5000"
depends_on:
- mongo
networks:
- app-network
mongo:
image: mongo
container_name: mongo
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password123
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
networks:
- app-network
mongo-express:
image: mongo-express
container_name: mongo-express
restart: unless-stopped
environment:
ME_CONFIG_MONGODB_SERVER: mongo
ME_CONFIG_MONGODB_ADMINUSERNAME: admin
ME_CONFIG_MONGODB_ADMINPASSWORD: password123
ports:
- "8081:8081"
depends_on:
- mongo
networks:
- app-network
networks:
app-network:
volumes:
mongo-data:
Enter fullscreen mode Exit fullscreen mode
Now we have an entire application architecture described in one file:
Docker Compose
|
+------------+------------+
| | |
Flask App MongoDB Mongo Express
| | |
+------------+------------+
|
app-network
|
mongo-data
volume
Enter fullscreen mode Exit fullscreen mode
Start everything with:
docker compose up -d
Enter fullscreen mode Exit fullscreen mode
Check the services:
docker compose ps
Enter fullscreen mode Exit fullscreen mode
View all logs:
docker compose logs
Enter fullscreen mode Exit fullscreen mode
Stop everything:
docker compose down
Enter fullscreen mode Exit fullscreen mode
And bring it back:
docker compose up -d
Enter fullscreen mode Exit fullscreen mode
Your MongoDB data remains because the named volume wasn’t removed.
Why Compose Is a Big Step Forward
Before Compose, your workflow looked like this:
Remember commands
↓
Create network
↓
Create volume
↓
Start database
↓
Start application
↓
Start admin UI
↓
Check configuration
Enter fullscreen mode Exit fullscreen mode
With Compose:
docker compose up -d
↓
Everything
Enter fullscreen mode Exit fullscreen mode
More importantly, the architecture is now stored as code.
You can:
- Commit it to Git
- Review changes
- Share it with teammates
- Reproduce the environment
- Use it in development
- Use it in CI/CD workflows
That’s a major shift in how you manage applications.
Docker Compose Is Infrastructure as Code Practice
You may not think of a Compose file as infrastructure code yet.
But it is teaching you the right mindset.
Instead of:
“I remember how I configured this server.”
You say:
“The configuration is defined in Git.”
Instead of:
“Run these 12 commands in this exact order.”
You say:
“Here is the desired state.”
This mindset becomes extremely important when you move into:
- CI/CD
- Kubernetes
- Terraform
- Ansible
- Cloud infrastructure
Docker Compose is one of the first practical places where you’ll experience this shift.
Final Thoughts
Docker made it easy to package applications into containers.
Docker networking made it possible for containers to communicate.
Docker volumes made persistent data possible.
And Docker Compose brings all of these concepts together into one manageable application definition.
The biggest lesson today isn’t just learning:
docker compose up
Enter fullscreen mode Exit fullscreen mode
It’s learning to describe infrastructure declaratively.
Instead of manually managing containers one by one, you define the entire application stack as code and let Docker Compose handle the implementation.
That’s the real bridge from simply using Docker to thinking like a DevOps engineer.
Define it once. Version it. Share it. Reproduce it.