example-projects/reference/docker/09_compose_basics.md

165 lines
5.0 KiB
Markdown
Raw Normal View History

# Lesson 09: Docker Compose — Putting the Run Command in a File
Recall the last `docker run` from lesson 07:
```bash
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`:
```yaml
services:
web:
image: nginx
ports:
- "8080:80"
```
That's it. Three nested lines describe an entire deployment.
In the same folder:
```bash
docker compose up
```
What happens:
- Compose reads the file.
- It pulls `nginx` if needed.
- It creates a network for this stack.
- It starts a container called `web` with 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:
```bash
docker compose down
```
The whole stack comes down.
## Run in the background
Add `-d` (detached):
```bash
docker compose up -d
```
The stack starts and returns your terminal immediately. To see logs:
```bash
docker compose logs -f
```
To stop:
```bash
docker compose down
```
## A more realistic single-service file
Translating the long `docker run` from the start of this lesson:
```yaml
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 up` and have the same thing running.
- `restart: unless-stopped` means "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:
```yaml
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/image_meaning_db/docker-compose.yml)
- [`../../examples/audio_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
1. Create a folder, put a `docker-compose.yml` with the simple nginx example in it, and `docker compose up`. Visit it in your browser.
2. Add another service to the same file — say, a second nginx on port 8081:
```yaml
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 down` brings them both down at once.
3. Move on to [`10_compose_multi_service.md`](10_compose_multi_service.md) where the services actually talk to each other.