platform/docs/07-repo-layout-gitops.md
Bart Van Geyt e5ff798dd4 Refine to nested customer -> site model
Adopt a two-level ownership model: a customer owns many sites, with
isolation/containers/DB/backup at the site level and grouping (one SFTP
login, recursive backup, billing, bulk delete) at the customer level.

- 03-naming-conventions: rewritten for customer/site ids and slug =
  <customer>-<site>; nested ZFS, per-customer SFTP chroot, deployments
  mirror the nesting.
- Propagated paths through docs 01, 04, 05, 06, 07 and deployments/README.
- ADR 0005 retitled/updated to per-site datasets nested under customer.
- architecture-plan.md: note pointing to doc 03 as authoritative on naming.

Clarifies subdomains: same-app subdomains are aliases on one site; a
separate-app subdomain is its own isolated site under the same customer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 12:37:39 +02:00

3.8 KiB

07 — Repository Layout & GitOps

1. Why a monorepo with clear boundaries

Platform code, image templates, per-customer deployment state, and secrets have different lifecycles and audiences. We keep them in one repository (simple to reason about for a small team) but as strictly separated top-level areas, so they can be split into independent repos later without restructuring.

heleosv2/
├── docs/              # this design set (ADRs, runbooks, conventions)
├── platform-infra/    # Ansible + base compose for host & platform services
├── site-templates/    # Dockerfiles + compose templates per site profile
├── deployments/       # rendered per-customer configs (GitOps state)
└── control-panel/     # provisioning CLI now; customer panel later

platform-infra/

Host baseline and platform services as code:

  • Ansible roles: ZFS pool/datasets, Docker, nftables (Docker-aware), SSH hardening, egress filtering, automysqlbackup, ZFS snapshot/send jobs.
  • Base compose projects: Traefik, shared MariaDB, Forgejo + registry, Prometheus/Grafana/Loki, Uptime-Kuma.

site-templates/

The building blocks the CLI renders from:

  • Dockerfiles for standard images (php-fpm non-root, nginx, static base).
  • One compose template per profile (static, redirect, custom-php, wordpress) with placeholders filled from site.yaml.
  • Image builds run through CI with Trivy + gitleaks; images pushed to the Forgejo registry.

deployments/

The GitOps state — one directory per site (see 03 §7). Rendered docker-compose.yml, .env, encrypted secrets.enc.yaml, and the declarative site.yaml. Committing here is the audit trail of what is deployed. Never commit plaintext secrets.

control-panel/

The provisioning CLI (provision/reconfigure/deprovision/backup/restore/ …). Reads/writes site.yaml, renders from site-templates/, writes to deployments/, and drives ZFS/DB/Docker. The future web panel lives here too, calling the same operations.

2. Secrets

  • SOPS + age encrypt secrets at rest; only *.enc.* / *.sops.* files are committed (enforced by .gitignore).
  • Plaintext .env files are git-ignored; a decrypt step materializes runtime env just before compose up (and it stays out of the customer ZFS dataset).
  • Per-site DB/SFTP credentials are unique and rotatable (rotate-secret).
  • The age private key is an operator secret, stored outside the repo and part of DR (without it, encrypted secrets are unrecoverable — back it up offline).

3. GitOps flow

edit site.yaml ─▶ CLI renders ─▶ commit deployments/<cust>/<site> ─▶ compose up -d
     (intent)      (from templates)     (audit trail)          (converge)
  • Source of truth: site.yaml per site + the base compose in platform-infra.
  • Change = a commit in deployments/. History shows who deployed what, when.
  • CI (Forgejo Actions): lint/scan templates and images; optionally validate that deployments/ renders cleanly from site.yaml. Deployment stays operator-triggered on the single host initially (no auto-apply agent yet).

4. Branching

  • main is deployable. Platform/template changes go via short-lived branches + PR + CI (scans must pass).
  • deployments/ commits may be direct on main (operational changes) but still run the render/scan checks.

5. When to split into multiple repos

Split when any becomes true: multiple operators needing different access to deployments/ vs platform code; deployments/ history dominating the repo; or open-sourcing site-templates/control-panel while keeping deployments private. The top-level separation above makes that a clean git filter-repo extraction rather than a rewrite.