Why the Infra Repo Should Never Contain Your Data
nextcloud/, nextcloud_data/, db/, and redis/ are bind-mounted directories that live entirely on vmi2633427, are gitignored on purpose, and are explicitly not backed up by the repo that describes the stack running on top of them.
The nextcloud-selfhosted README opens with a sentence that's easy to read past: "Config only. The bind-mounted data (nextcloud/, nextcloud_data/, db/, redis/) lives on the server, is gitignored, and is not backed up by this repo." That's not a limitation being apologized for — it's the design.
Git holds everything needed to rebuild the stack. It holds none of what the stack has stored.
What actually lives in the repo
docker-compose.yml the stack
Dockerfile optional custom image
.sops.yaml who can decrypt
secrets/db.enc.env ENCRYPTED — committed
scripts/ occ, secrets-{decrypt,encrypt,edit,rotate}, upgrade, maintenance_off
.github/workflows/ image build (opt-in) + validationEvery one of these files describes how the stack is assembled — which images run, how they're wired together, what credentials they need (encrypted) to start. None of it holds a single photo, document, or database row a user has ever created. That line is deliberate, and it's enforced by .gitignore rather than by convention alone: nextcloud/, nextcloud_data/, db/, and redis/ are all bind-mounted directories excluded from version control.
Why this split, specifically
Treating config and data as fundamentally different kinds of thing has a concrete payoff at the moment it matters most: rebuilding a dead or migrated host. git clone plus secrets-decrypt.sh plus docker compose up -d reproduces the entire stack — every setting, every credential, every image version — with zero manual reconstruction. None of that process touches the actual Nextcloud files, database contents, or Redis cache, because none of it needs to. Those come back from wherever the data backup strategy puts them — a separate, deliberate process with its own retention policy, not a side-effect of git push.
If config and data lived in the same place, every backup of one would silently also be a backup (or a leak) of the other, and every restore of one would risk clobbering the other. Keeping them apart means the two can be backed up on entirely different schedules, to entirely different destinations, by entirely different tooling, without either constraining the other's design.
The failure mode this prevents
git checkout -f, used when attaching a fresh clone of the repo to an already-running host, does clobber colliding untracked files — that's stated explicitly as a gotcha, with a mandatory tar czf backup step immediately before it in the server-setup procedure:
tar czf ~/nextcloud-config-backup-$(date +%F).tgz \
--exclude=./db --exclude=./nextcloud --exclude=./nextcloud_data \
--exclude=./redis --exclude=./nextcloud.old --exclude=./.git .Notice what's excluded from that tarball: the data directories, explicitly, by name. Even the emergency backup taken specifically because a git operation might clobber things respects the config/data boundary — it backs up config, on the assumption that data is protected by its own, separate mechanism, and the two backup jobs are never meant to merge into one.
The general principle
Any self-hosted stack has this same two halves — the description of the system, and the state the system has accumulated. The description belongs in version control: it's small, it's diffable, and every previous version is a rollback target. The state does not: it's usually large, it's not meaningfully diffable, and a git history full of database dumps is both an operational burden and, if the repo is ever exposed, a much bigger leak than a docker-compose.yml. Treating "what would let me rebuild this from nothing" and "what would let me recover what users actually created" as two separate questions, with two separate answers, is what keeps either one from silently becoming the other's problem.
Series: The Self-Hosting Stack. Next: auditing your own infrastructure and finding what you forgot.