5.0 KiB
Lesson 09: Docker Compose — Putting the Run Command in a File
Recall the last docker run from lesson 07:
docker run -d \
--name my-backend \
-p 8080:8080 \
-e DATABASE_URL=postgres://workshop:secret@db:5432/projects \
-e LOG_LEVEL=info \
-v ./uploads:/app/uploads \
my-backend-image:1.0
Imagine typing that — correctly — every time. Imagine a friend trying to run your project and you having to send them the right invocation through chat. Imagine having to spin up a backend and a database and a cache.
This is the problem docker compose solves. You describe what you want in a YAML file. Then you run docker compose up and Docker does the rest.
Your first compose file
Make a folder and put this in it as docker-compose.yml:
services:
web:
image: nginx
ports:
- "8080:80"
That's it. Three nested lines describe an entire deployment.
In the same folder:
docker compose up
What happens:
- Compose reads the file.
- It pulls
nginxif needed. - It creates a network for this stack.
- It starts a container called
webwith port 80 mapped to host 8080. - It streams logs to your terminal.
Open http://localhost:8080. There's nginx.
Press Ctrl+C to stop and (optionally) clean up with:
docker compose down
The whole stack comes down.
Run in the background
Add -d (detached):
docker compose up -d
The stack starts and returns your terminal immediately. To see logs:
docker compose logs -f
To stop:
docker compose down
A more realistic single-service file
Translating the long docker run from the start of this lesson:
services:
backend:
image: my-backend-image:1.0
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://workshop:secret@db:5432/projects
- LOG_LEVEL=info
volumes:
- ./uploads:/app/uploads
restart: unless-stopped
Same setup as the long shell command. But:
- It's version-controllable (commit it to git).
- A new person can clone your project and
docker compose upand have the same thing running. restart: unless-stoppedmeans "if this container crashes or the machine reboots, bring it back automatically." That's a real production-grade behavior with one line of YAML.
image: vs build:
A service can either pull an existing image or build one from a Dockerfile in the same project:
services:
backend:
build: ./backend # Look for a Dockerfile in ./backend/ and build it
ports:
- "8080:8080"
When you docker compose up, Compose will build the image as part of starting up. Edit the Dockerfile and run docker compose up --build to force a rebuild.
This is how most of the real-world Compose projects you'll see work: their own services use build:, and standard things like databases use image:.
The lifecycle commands
| Command | What it does |
|---|---|
docker compose up |
Start the stack, attach to logs. |
docker compose up -d |
Start the stack in the background. |
docker compose up --build |
Rebuild any build: services before starting. |
docker compose down |
Stop and remove containers and networks. Keeps volumes. |
docker compose down -v |
Same plus delete the named volumes. Destroys data — be deliberate. |
docker compose logs -f |
Tail the logs of every service. |
docker compose logs -f backend |
Logs of one service only. |
docker compose ps |
List the containers in this stack. |
docker compose exec backend bash |
Open a shell inside a running service. |
docker compose restart backend |
Restart one service. |
You can spend years writing software and use only these commands.
Where compose.yml lives
Compose looks for docker-compose.yml (or the newer name compose.yml — both work) in the current directory. Each project gets its own folder with its own compose file. Running docker compose up in different folders gives you independent stacks.
The two example projects in this repo are both Compose-based:
../../examples/image_meaning_db/docker-compose.yml../../examples/audio_meaning_db/docker-compose.yml
Both are small enough to read top-to-bottom in a minute. Cross-reference them with what you've learned so far and most lines should make sense.
Try it yourself
-
Create a folder, put a
docker-compose.ymlwith the simple nginx example in it, anddocker compose up. Visit it in your browser. -
Add another service to the same file — say, a second nginx on port 8081:
services: web1: image: nginx ports: - "8080:80" web2: image: nginx ports: - "8081:80"Run
docker compose up. Both are reachable on their respective ports.docker compose downbrings them both down at once. -
Move on to
10_compose_multi_service.mdwhere the services actually talk to each other.