
Docker Compose
Running a single container with docker run is straightforward. But real applications are rarely a single container.
A web application might have a frontend, a backend API, a database, and an authentication service. Getting all of those started, networked together, and configured correctly with individual docker run commands gets messy very quickly.
Docker Compose is the solution. It lets you define and manage multi-container applications in a single file.
The Idea Behind Compose
Modern applications often follow a microservices architecture. Rather than one large application doing everything, the application is broken into smaller services, each focused on one job. A database service, an API service, a frontend service, they’re all doing their specific part and working together.
This is what Docker Compose manages. You describe all of your services in one configuration file. One command starts them all. Another stops them all.
Even if you’re not building microservices, Compose is useful. It’s a much cleaner way to configure containers than remembering a long docker run command with a dozen flags.
YAML: The Language of Compose Files
Compose files are written in YAML. This is a simple text format for describing structured data. If you’ve worked with Ansible, Kubernetes, or similar tools, you’ve already seen it.
YAML uses key-value pairs separated by colons:
server_name: MyServer
ip_address: 192.168.1.10
Values can be nested under a key using indentation:
network:
subnet: 192.168.1.0
mask: /24
gateway: 192.168.1.1
Lists use a hyphen for each item:
ports:
- "8080:80"
- "443:443"
Indentation in YAML is mandatory and meaningful. Consistent spacing (usually two or four spaces) is required. Don’t mix tabs and spaces.
If you’re ever unsure whether your YAML is valid, paste it into an online validator like yamllint.com before trying to run it.
A Simple Compose File
Here’s the simplest possible example. A single NGINX container:
services:
web:
image: nginx:latest
ports:
- "8080:80"
services is the parent key for all your containers. Under it, web is the name we’ve chosen for this service. It’s not a special keyword.
image specifies which image to run. ports maps the host port to the container port, just like -p in docker run.
This is equivalent to running:
docker run -p 8080:80 nginx:latest
Save this as compose.yaml and run:
docker compose up -d
The -d flag runs it in detached mode. To stop and remove everything:
docker compose down
A More Complete Example
Here’s a more realistic example. This is a PostgreSQL database with Adminer (a web-based database manager) as the frontend:
services:
db:
image: postgres:16
container_name: demo_db
environment:
POSTGRES_DB: demoapp
POSTGRES_USER: demo_user
POSTGRES_PASSWORD: demo_password
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
adminer:
image: adminer:latest
container_name: demo_adminer
depends_on:
db:
condition: service_healthy
environment:
ADMINER_DEFAULT_SERVER: db
restart: unless-stopped
ports:
- "8080:8080"
Let’s look at the key features this introduces.
environment
The environment section sets environment variables on the container at runtime. This is how we pass configuration to the database; The database name, username, and password.
Setting these at runtime (in the Compose file) rather than at build time (in the Dockerfile) is important. Values in a Dockerfile are baked into the image and can be inspected by anyone who has access to the image. Runtime values are not.
restart
restart: unless-stopped tells Docker to automatically restart this container if it stops, unless we stopped it manually. Other options are no, always, and on-failure.
healthcheck
The healthcheck section defines a command Docker runs periodically to check whether the container is actually working, not just running.
In this case, it runs a PostgreSQL-specific command to check if the database is ready to accept connections. Docker reports the container as healthy or unhealthy based on the result.
depends_on
depends_on tells Compose that one service must be ready before another starts.
With the condition: service_healthy option, Adminer won’t start until the database is reporting as healthy, not just running. This ensures the database is actually ready to accept connections before the frontend tries to connect.
Keeping Secrets Out of Compose Files
The example above puts the database password directly in the Compose file. That’s acceptable for a home lab, but not ideal for anything more sensitive.
Another approach is a .env file — a separate text file containing your environment variables:
DB_USER=demo_user
DB_PASS=demo_password
Docker Compose automatically reads .env from the same directory. You can then reference these values in your Compose file:
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
This keeps your credentials out of the main Compose file. You can set tighter permissions on the .env file, and you avoid accidentally committing passwords to a code repository.
Another alternative is to include the passwords for the first run of the application only. Many services will setup a local database and store the passwords there. Once initial setup is complete, go back to the compose file and remove the passwords.
Building Images Within Compose
You don’t have to use pre-built images. Compose can build an image from a Dockerfile as part of starting the application:
services:
hello:
build:
context: .
dockerfile: Dockerfile
image: my-compose-image:latest
container_name: hello_from_compose
When you run docker compose up, Compose builds the image first, then starts the container.
One thing to know: if the image already exists, Compose won’t rebuild it. To force a rebuild:
docker compose up -d --build
Useful Commands
| Command | Description |
| docker compose up -d | Start all services in the background |
| docker compose down | Stop and remove all services |
| docker compose ls | List running Compose applications |
| docker compose logs | View logs from all services |
| docker compose logs -f | Follow logs in real time |
| docker compose up –build | Rebuild images before starting |
| docker compose up –force-recreate | Restart all containers even if running |
Where to Go Next
The full course includes hands-on labs with multi-container applications, including a complete home lab build using Docker Compose:
Watch the free YouTube course → COMING SOON
Enrol in the full Udemy course → COMING SOON