What Is Docker Compose
In the previous module, you deployed one container at a time, manually, with docker run. It works for one container. But what if your site needs three at the same time: the app, the database, and a cache? Type three huge commands every time and remember each port, volume, and connection? Forget it. The Docker Compose solves this: you describe everything in a file and bring up the entire stack with a single command.
🧠 Analogy: The Orchestra Conductor
Imagine an orchestra. Each musician (container) knows how to play their instrument on their own: the app plays, the database plays, the cache plays. But if everyone starts whenever they want, the result is noise. The maestro (Compose) reads the score (the file docker-compose.yml) and governs them all together: who starts first, who talks to whom, and in what tone.
- •A sheet music = the file
docker-compose.yml - •The musicians = each service (app, database, cache)
- •O "start playing" =
docker compose up
💡 Docker Compose is already included
If you installed modern Docker (Docker Desktop or the recent Docker Engine on Linux), Compose already installed as a subcommand: you call docker compose (with a space). On older machines, the command was docker-compose (with a hyphen). Both do the same thing; in this module, we use the newer form with a space.
# Check whether Compose is available
$ docker compose version
Docker Compose version v2.27.0
The Basic docker-compose.yml File
All the magic lives in a single text file called docker-compose.yml. It’s written in YAML, a simple format based on indentation (spaces at the beginning of the line). The golden rule: indentation determines what goes inside what. We’ll go from the simplest to the most complete.
The smallest possible compose file: one service
# docker-compose.yml
services:
app:
image: nginx:alpine
ports:
- "8080:80"
This describes a service called app, which uses the image nginx:alpine exposes the container’s port 80 on port 8080 of your machine.
services
The list of all containers in the stack. Each key here is a service.
image
A ready-to-use image (from Docker Hub). Alternative: build: to build from your Dockerfile.
ports
Maps ports in the format "host:container". The one on the left and the one on your PC.
💡 About the “version:” at the top
In older tutorials, you’ll see a line version: "3.8" at the beginning of the file. In modern Compose (v2), this line is optional and ignored: you can start directly at services:. If an example file has the version:, don't worry, it still works.
⚠️ Common Error
Problem: "yaml: line 4: mapping values are not allowed in this context" or the stack won't start.
Solution: Almost always, it’s incorrect indentation. YAML doesn't accept Tab, just spaces. Always use 2 spaces per level and never mix tabs and spaces. In VS Code, turn on "show whitespace" to spot the problem.
✓ What TO DO
- ✓Always use 2 spaces per indentation level
- ✓Using fixed versions for the images (
nginx:1.27) - ✓Give services clear names (
app,banco)
✗ What NOT to do
- ✗Use Tab to indent (breaks YAML)
- ✗Use only
:latestin production (it becomes a surprise) - ✗Forget the quotation marks in
"8080:80"
Services, Networks, and Volumes
Three concepts solve 90% of cases: services (the containers), networks (how they communicate) and volumes (where the data is stored). The great thing about Compose: it creates an automatic network for your stack, and inside it one service calls the other by name.
👁 What you’ll see: communication by name
You don’t use an IP. If the database service is called banco, the app connects to it through the URL using the service name as the address:
# Inside the app container, the database connection looks like this:
DATABASE_URL=postgres://user:senha@database:5432/meubanco
# "banco" is the service name, NOT an IP. Compose resolves it.
Network: ready to go by default
When you bring up the stack, Compose creates a network just for it. All services join that network automatically and can reach each other by name. You only declare networks manually when you want to isolate groups (e.g., separate the frontend from the backend).
services:
app:
image: my-app
depends_on:
- database # ensures the database starts first
Volume: so the data doesn’t disappear
A container is disposable: if you remove the database container, the data goes with it. The volume stores the data outside from the container, in a place that survives recreation. Declare it at the root and use it inside the service:
services:
database:
image: postgres:16
volumes:
- dados-db:/var/lib/postgresql/data
# Volume declaration at the root of the file:
volumes:
dados-db:
Format: nome-do-volume:/caminho/dentro/do/container. Even if you take down and recreate the container, the data stays there.
⚠️ Common Error
Problem: "Every time I run docker compose down my bank resets."
Solution: You’re missing the volume. Without a volume, the data lives inside the container and disappears when it's removed. Declare a named volume for the database. Also watch out for down -v: o -v deliberately deletes the volumes.
Bring Up a Complete Stack: App + Database
It's time to bring everything together. Let's build a real stack: a web app that communicates with a PostgreSQL database, with a volume so the data doesn't disappear. One file, one command, two containers talking to each other.
The complete docker-compose.yml
# docker-compose.yml
services:
app:
build: . # builds from the local Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:senha@database:5432/app
depends_on:
- database
database:
image: postgres:16
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=senha
- POSTGRES_DB=app
volumes:
- dados-db:/var/lib/postgresql/data
volumes:
dados-db:
Bring up the entire stack
O -d ("detached") releases the containers running in the background
$ docker compose up -d
[+] Running 3/3
✓ Network meuapp_default Created
✓ Container meuapp-banco-1 Started
✓ Container meuapp-app-1 Started
Check that it went up
Lists the stack's containers and the status of each one
$ docker compose ps
NAME SERVICE STATUS PORTS
meuapp-app-1 app Up 12 seconds 0.0.0.0:3000->3000/tcp
meuapp-banco-1 banco Up 13 seconds 5432/tcp
Take it down when you’re done
Stop and remove the containers and network (but keep the data volume)
$ docker compose down
[+] Running 3/3
✓ Container meuapp-app-1 Removed
✓ Container meuapp-banco-1 Removed
✓ Network meuapp_default Removed
💡 Tip: run up without -d to see everything at once
The first time, run docker compose up without o -d. The logs from all services appear mixed together on the screen, live. Great for seeing if anything went wrong during startup. When everything is set, bring it down with Ctrl+C and bring it back up with -d.
Logs and Debugging: When Something Breaks
Did you bring up the stack and the site won’t open? Don’t worry, that’s normal. Compose gives you three tools to investigate: view the logs (what each service is saying), see the state of the containers, and go inside from a container to take a closer look.
logs - Read what the services say
# Logs for all services, following live (-f = follow)
$ docker compose logs -f
banco-1 | database system is ready to accept connections
app-1 | Server listening on port 3000
# Logs for just ONE service
$ docker compose logs app
# Only the last 50 lines
$ docker compose logs --tail 50 banco
The log is the first place to look. 9 out of 10 problems show up here in plain text.
ps - Check the status of each container
$ docker compose ps
NAME SERVICE STATUS PORTS
meuapp-app-1 app Exited (1) 4 seconds ago
meuapp-banco-1 banco Up 20 seconds 5432/tcp
Here, the app is set as Exited (1): it went down. The next step is to check its log with docker compose logs app.
🔎 exec - Enter a container
Sometimes you need to look inside: run a command, open the database client, check a file. The exec run a command inside a container that is already running:
# Open a terminal inside the app container
$ docker compose exec app sh
/app # ls
node_modules package.json server.js
# Open the PostgreSQL client inside the database
$ docker compose exec banco psql -U user -d app
app=# \dt
⚠️ Common Error
Problem: "The app starts, but can’t connect to the database: connection refused."
Solution: O depends_on ensures the order of startup, but doesn’t wait for the database to become ready to accept connections (it takes a few seconds). Simple solution: make the app try to reconnect a few times. Robust solution: use healthcheck in the database + condition: service_healthy in depends_on.
Updates and Rollback
Your application is live, and you want to publish a new version. Or worse: you published it and something went wrong; you need to go back fast. With Compose, updating and rolling back versions take just a few commands. The key is to pin the image tag to always know which version is running.
Update to a new version
Suppose your image in the file was meu-app:1.4 and the new version and the 1.5. You change the tag in the file and update only what changed:
# 1. Edit the compose file: image: meu-app:1.4 -> image: meu-app:1.5
# 2. Download the new image from the registry
$ docker compose pull
✓ app Pulled
# 3. Recreate only the containers that changed
$ docker compose up -d
[+] Running 2/2
✓ Container meuapp-banco-1 Running
✓ Container meuapp-app-1 Recreated
Notice: the database stays Running (you didn’t change it) and only the app is Recreated. Compose only touches what changed.
Rollback: return to the previous version
Did things go wrong in 1.5? Move the tag back to 1.4 and upload it again. Since you pinned the version, you know exactly where to go back to.
# Edit the compose file back: image: meu-app:1.5 -> image: meu-app:1.4
$ docker compose up -d
✓ Container meuapp-app-1 Recreated
# Ready: in seconds, you're back on version 1.4
✓ What TO DO
- ✓Pin version tags (
app:1.4) so you can go back - ✓Run
pullbefore theup -d - ✓Store the compose file in Git (each version becomes history)
✗ What NOT to do
- ✗Use
:latest(you can’t tell which version to roll back to) - ✗Run
down -vthinking all you have to do is restart - ✗Update directly in production without testing first
🏆 You can now run a real stack
With the app, database, and volume in one file, starting everything with one command, reading logs, accessing containers, and rolling back a version when needed, you already have the essentials of local orchestration. In the next module, you'll learn how to build this stack start on its own when the server starts, with the systemd.
📚 Module Summary
Next Module:
4.3 - Systemd: making your stack start automatically when the server boots and restart if it goes down