Files
hq/04-ISSUES/026-the-data-directories-are-not-declared/00-report.md
T
jschoubben bd8f09d647 026 fixed — and the coincidence turned into a rule
The fourteen undeclared mounts are declared. More to the point, a
manifest that does not declare one is now refused: they were right by
coincidence, and a checklist nothing enforces is a checklist that is
true until the next commit.

Refused in the control plane, because the machine cannot tell the
difference — asked to mount a path that does not exist, it makes the
directory, which is a thing it is perfectly able to do.
2026-09-01 19:36:15 +02:00

86 lines
4.1 KiB
Markdown

---
status: fixed
opened: 2026-09-01
located-in: [mesh-control]
fixed-by: mesh-control 53eb000
amended-design:
---
# 026 — The data directories are mounted and never declared
## Symptom
Four modules mount **fourteen host paths** that no resource in those modules declares:
```
gitea /services/gitea/gitea
postgres /services/postgres/db-data
minio /services/minio/data/data1-1
mailu eleven more, including the mail spool and the admin database
```
Each is a bind mount on a container. None is a `directory` resource. The mesh has never heard of
any of them.
## What that costs
**They are created by the container runtime, as root.** A bind mount whose source does not exist
is created for you, owned by root, with whatever mode the runtime picks. So `owner` and `mode` —
which exist precisely so a module can say who its data belongs to — are silently not applied to
the only directories that hold data.
**The protection that exists for exactly this does not reach them.** A directory the mesh declared
and no longer wants is *kept*, not removed, when it holds anything the mesh did not put there
([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md)). That rule is the
answer to *what happens to my data when a module goes away*, and it is written in terms of
declared directories. **An undeclared one is not protected by it, because the mesh does not know
it is there.**
So the single rule guarding against data loss covers the configuration directories, which are
cheap to lose, and not the data directories, which are the reason the rule exists.
## Where it came from
These manifests were written by reading the arrangement being replaced and carrying its
`docker-compose` files across — service, image, ports, volumes, environment — into the new
manifest's container shape. That shape can express all of it, which is what made the
transliteration feel like progress.
**A container shape that can express a compose file will be filled in like a compose file.** The
mesh's model is larger than that: a directory is a thing the mesh owns, with an owner and a mode
and a rule about what happens when it is no longer wanted. A volume line borrowed from compose
declares none of it, and nothing complains, because a bind mount source is a string.
## What a fix has to keep
- **Every host path a container mounts is declared.** If a module wants a directory on the
machine, it says so, with who owns it and what mode — and gets the removal rule with it.
- **The check is mechanical.** A person comparing volumes against declared directories by hand is
the process that produced this. It is a few lines against the manifest and belongs beside the
other manifest checks.
- **Not by inventing directories at apply time.** The host creating what a mount needs would make
the mesh's ownership of a directory depend on which resource mentioned it first, and would put
the same undeclared path back a layer down.
## Not yet answered
**Where a module's data should live at all.** These paths were inherited whole from the
arrangement being replaced, which put everything under one directory per service. Whether that is
right here is a separate question, and a bigger one — it decides what a person backs up, and what
survives a module being removed.
## Fixed
**All fourteen are declared**, across the forge, the mail system, the store and the object store —
each mount now resolves to a `directory` or to a file the module already names.
**And a manifest that does not declare one is refused**, where it is written. The manifests being
right today was a coincidence: nothing said they had to be, so the next volume somebody added
would have been undeclared again and nothing would have said so. A path under a declared directory
counts as declared, as do a module's own secrets, its grants, and what it receives.
Refused in the control plane rather than on the machine, which cannot tell the difference: by the
time the host sees the mount it is being asked to create a directory, which it is perfectly able to
do. The fault is in the manifest, so it is named at the manifest — the same argument as the action
refusal it now sits beside.