# 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](03-naming-conventions.md) §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`](../.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// ─▶ 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.