Implements the two decoupled backup streams from docs/06 as an idempotent
Ansible role wired into the host playbook:
- Files: sanoid takes/prunes ZFS snapshots per policy (sanoid_datasets) on
its packaged timer; syncoid replicates offsite (heleos-zfs-offsite),
enabled only when zfs_offsite_target is set.
- DB: heleos-db-backup (nightly systemd timer) walks the deployments dir and
dumps each DB-backed site via `docker exec mariadb-dump` into
db-backups/<customer>/<site>/{daily,weekly,monthly} with rotation
(automysqlbackup-style, adapted for the containerized DB; MYSQL_PWD keeps
the password out of the process list). heleos-db-offsite rsyncs offsite
when db_offsite_target is set.
Streams and schedules are configured in group_vars/all.yml; offsite is
opt-in via the two target vars. Updates doc 06 (implementation note), the
ansible README, and CLAUDE.md status. YAML + templates validated by render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
4.1 KiB
Markdown
72 lines
4.1 KiB
Markdown
# CLAUDE.md — heleosv2 platform
|
||
|
||
Multi-tenant web hosting platform: **container-per-site + ZFS-dataset-per-site**,
|
||
CLI-driven provisioning, decoupled per-site backup/restore. Single bare-metal
|
||
host, Docker Compose per site, no orchestrator.
|
||
|
||
> **Read `docs/` first — it is the source of truth.** Start with
|
||
> [docs/00-roadmap.md](docs/00-roadmap.md). This file is just the orientation
|
||
> map so a fresh session gets up to speed cheaply.
|
||
|
||
## Core philosophy
|
||
1. **The customer is the boundary** — isolation, backup, restore align on it.
|
||
2. **Two levels:** a `customer` owns many `site`s. Isolation/containers/DB/network
|
||
are per **site**; grouping (one SFTP login, recursive backup, billing) is per
|
||
**customer**. See [docs/03-naming-conventions.md](docs/03-naming-conventions.md).
|
||
3. **Containers isolate, they don't secure by themselves** — non-root FPM,
|
||
read-only rootfs, per-site networks, egress filtering, least-priv DB users.
|
||
4. **Each backup stream matches its change pattern** — web files via ZFS
|
||
snapshot/`send`; DB via automysqlbackup + rsync; logs via Loki. Never fold the
|
||
DB dump into the web dataset (it bloats every incremental).
|
||
5. **CLI/templates before UI.** Prove the platform, then wrap it in a panel.
|
||
|
||
## Status
|
||
- ✅ Phase 0 — design docs, ADRs, scaffold ([docs/](docs/00-roadmap.md))
|
||
- ✅ Phase 1 — host baseline Ansible ([platform-infra/ansible/](platform-infra/ansible/README.md))
|
||
- ✅ Phase 2 — platform services (Traefik, MariaDB, Forgejo) in [platform-infra/stacks/](platform-infra/stacks/README.md)
|
||
- ✅ Phase 3 — site templates & base images in [site-templates/](site-templates/README.md)
|
||
- ✅ Phase 4 — provisioning CLI `heleosctl` in [control-panel/](control-panel/README.md) (provision/deprovision/list/render/backup/restore; `--dry-run`; 22 tests)
|
||
- ✅ Phase 5 — backup/DR automation in the `backup` Ansible role (sanoid snapshots, per-DB dump timer, optional syncoid/rsync offsite)
|
||
- 🚧 Phase 6 — observability (Prometheus/Grafana + cAdvisor/node_exporter/Traefik metrics, Loki/Promtail, Uptime-Kuma)
|
||
- ⬜ Phases 7–8 — migration, customer panel
|
||
|
||
### Phase 4 follow-ups (not yet done)
|
||
- `reconfigure` + `rotate-secret` commands; git-commit of `deployments/` on provision; php-fpm umask for two-way SFTP editing (see control-panel/README.md).
|
||
|
||
## Repo map
|
||
| Path | Purpose |
|
||
|------|---------|
|
||
| `docs/` | Architecture, ADRs, runbooks, conventions (**authoritative**). |
|
||
| `platform-infra/ansible/` | Phase 1 host baseline (ZFS, Docker, firewall, SSH). |
|
||
| `platform-infra/stacks/` | Phase 2 base compose projects (Traefik/MariaDB/Forgejo). |
|
||
| `site-templates/` | Dockerfiles + per-profile compose templates (Phase 3). |
|
||
| `deployments/` | Rendered per-site configs `<customer>/<site>/` (GitOps state). |
|
||
| `control-panel/` | Provisioning CLI (Phase 4), panel later. |
|
||
|
||
## Naming quick-ref (see docs/03)
|
||
- `customer` + `site` ids; `slug = <customer>-<site>` (Docker/DB key).
|
||
- ZFS: `tank/customers/<customer>/<site>/web`; platform on `tank/platform/*`.
|
||
- DB: `db_<customer>_<site>` + user `u_<customer>_<site>` on shared MariaDB.
|
||
- Networks: shared `proxy` (edge) + `platform` (DB); per-site `<slug>_net`.
|
||
|
||
## Common commands
|
||
```bash
|
||
# Phase 1 — host baseline (from platform-infra/ansible, against the VM)
|
||
ansible-galaxy collection install -r requirements.yml
|
||
ansible-playbook site.yml # tags: base|zfs|docker|firewall|ssh
|
||
|
||
# Phase 2 — platform services (on the host, once Phase 1 is applied)
|
||
platform-infra/stacks/bootstrap-networks.sh
|
||
cd platform-infra/stacks/<svc> && cp .env.example .env && docker compose up -d
|
||
```
|
||
|
||
## Working agreements
|
||
- **Docs are source of truth.** When a design decision changes, update the
|
||
relevant `docs/` file and add/adjust an ADR in `docs/adr/`.
|
||
- **Never commit plaintext secrets.** Only `*.enc.*` (SOPS/age) are allowed;
|
||
`.env` is git-ignored (`.env.example` is committed). See `.gitignore`.
|
||
- **Line endings:** LF for scripts/Dockerfiles/YAML (enforced by `.gitattributes`)
|
||
— these run on Linux.
|
||
- **Commit per phase/step** with a descriptive message; the repo is the memory.
|
||
- Environment is Windows (authoring) → Linux host (runtime). Prefer the Bash tool
|
||
for POSIX one-offs.
|