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
to the source:
**Rule: any service that needs to keep state between container restarts
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/
mysql/ # mysql:8.0 datadir
redis/ # redis dump.rdb
… # whatever else needs persistence
```
The folder is gitignored (`docker-data/` in `.gitignore`). It survives
`docker compose down`. The deploy script `mkdir -p`s it on first run so
fresh boxes Just Work.
The folder is gitignored (`docker-data/` in `.gitignore`). The deploy
script `mkdir -p`s it on first run so 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
repo root is the entire prod state.
- 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.
- The repo path identifies which app owns the data when you have ten
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
```
---