U principle §6: bind mounts only, never named volumes

Promote 'don't use named volumes' from a mysql/redis-specific
explanation to a hard rule that applies to anything needing
persistence (ssh host keys, uploads, queue state, etc). Lead
with the 'docker compose down -v wipes named volumes' rationale
since that's the keystroke this rule is really protecting against.
Add a counter-pattern example.
This commit is contained in:
Fabian Wagner 2026-05-28 10:57:13 +02:00
parent 1d1a3b32f9
commit 2589d4baf4
1 changed files with 42 additions and 8 deletions

View File

@ -393,30 +393,64 @@ that's why each one starts with the app's slug.
--- ---
## 6. Persistent data: `./docker-data/` in the repo, gitignored ## 6. Persistent data: `./docker-data/` bind mounts, never named volumes
Bind-mount mysql and redis storage to a `docker-data/` folder right next **Rule: any service that needs to keep state between container restarts
to the source: gets a bind mount under `./docker-data/<service>/`. Never a named docker
volume.** This applies to mysql, redis, ssh host keys, app uploads,
queue state, every "this dir needs to survive" case — same shape, same
location, no exceptions.
``` ```
docker-data/ docker-data/
mysql/ # mysql:8.0 datadir mysql/ # mysql:8.0 datadir
redis/ # redis dump.rdb redis/ # redis dump.rdb
… # whatever else needs persistence
``` ```
The folder is gitignored (`docker-data/` in `.gitignore`). It survives The folder is gitignored (`docker-data/` in `.gitignore`). The deploy
`docker compose down`. The deploy script `mkdir -p`s it on first run so script `mkdir -p`s it on first run so fresh boxes Just Work.
fresh boxes Just Work.
### Why not named docker volumes? ### Why bind mounts, not named volumes
The headline reason: **`docker compose down -v` wipes named volumes**.
That command is in too many people's muscle memory ("nuke the stack and
start clean") for the production datastore to be one accidental keystroke
away from gone. Bind mounts under the repo are immune — `down -v`
doesn't touch them.
The rest of the rationale:
- Trivially backed up — `tar -caf data.tar.xz docker-data/` from the - Trivially backed up — `tar -caf data.tar.xz docker-data/` from the
repo root is the entire prod state. repo root is the entire prod state.
- Survives container/image churn including accidental - Survives container/image churn including accidental
`docker volume prune -af`. `docker volume prune -af` and `docker system prune --volumes`.
- Discoverable — anyone with the repo can see where the data lives. - Discoverable — anyone with the repo can see where the data lives.
- The repo path identifies which app owns the data when you have ten - The repo path identifies which app owns the data when you have ten
apps on one host. apps on one host.
- Restoring on a new host is `git clone && rsync docker-data/` — no
per-volume `docker volume create && docker run --rm -v ... tar` dance.
### Counter-pattern (do not do this)
```yaml
# WRONG — named volume; `docker compose down -v` deletes the data.
services:
mysql:
volumes:
- mysql-data:/var/lib/mysql
volumes:
mysql-data:
```
```yaml
# RIGHT — bind mount; survives `down -v`.
services:
mysql:
volumes:
- ./docker-data/mysql:/var/lib/mysql
```
--- ---