Establish the design foundation for the heleosv2 multi-tenant hosting platform before any implementation code: - Monorepo skeleton: docs/, platform-infra/, site-templates/, deployments/, control-panel/ with orientation READMEs. - docs/: roadmap index, architecture + threat model, naming conventions, site profiles, provisioning workflow, backup & DR runbook, repo/GitOps layout, and the approved architecture plan. - docs/adr/: 9 ADRs recording the rationale for single-host Compose, Traefik edge, nginx+fpm split, shared MariaDB, ZFS-per-customer, decoupled backup streams, Forgejo, CLI-first, and SFTP-only. - Secrets hygiene: .gitignore (only *.enc.* committed) and .gitattributes (LF for scripts/Dockerfiles/YAML run on the Linux host). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.6 KiB
03 — Naming & Conventions
Consistent, predictable names are what make CLI-driven provisioning and scripted backup/restore reliable. Every resource for a site is derivable from a single slug.
1. The slug
Each site has one canonical slug: lowercase, ASCII, [a-z0-9-], 3–32 chars,
starting with a letter. Derived from the primary domain (dots → hyphens) or set
explicitly.
example.com→example-comblog.example.com→blog-example-com
The slug is the join key across ZFS, Docker, database, and deployment config. It never changes for the life of the site (renaming = new slug + migration).
A customer may own several sites. Where a customer grouping is needed (billing, SFTP account), use a separate
customerid with the same charset rules. The default is one slug per site.
2. ZFS datasets
tank/
├── customers/
│ └── <slug>/
│ └── web # web root ONLY (bind-mounted into the site)
└── platform/
├── docker # Docker data-root (images/layers/volumes)
├── mariadb # shared MariaDB datadir
├── db-backups/<slug> # automysqlbackup output per site
├── traefik # ACME store + dynamic config
├── forgejo # Git + registry data
└── monitoring # Prometheus/Loki data
Snapshots: tank/customers/<slug>/web@auto-YYYYMMDD-HHMM.
3. Docker resources
| Resource | Pattern | Example |
|---|---|---|
| Compose project | <slug> |
example-com |
| Container | <slug>_<role> |
example-com_nginx, example-com_fpm |
| Per-customer network | <slug>_net |
example-com_net |
| Shared edge network | proxy (fixed) |
proxy |
| Platform network | platform (fixed) |
platform (MariaDB, monitoring) |
| Named volume (if used) | <slug>_<purpose> |
example-com_fpmtmp |
Traefik router/service labels also key off the slug:
traefik.http.routers.<slug>.rule=Host(...).
4. Database (shared MariaDB)
| Resource | Pattern | Example |
|---|---|---|
| Database | db_<slug_underscored> |
db_example_com |
| DB user | u_<slug_underscored> |
u_example_com |
| Grants | ALL PRIVILEGES ON db_<...>.* to that user only |
— |
Hyphens in the slug become underscores for MySQL identifiers
(example-com → example_com). Passwords are generated, stored encrypted
(SOPS/age), never reused across sites.
5. Domains & TLS
- Primary domain drives the slug; additional aliases are listed in the site's deployment config and added to the Traefik Host rule.
- Certificates are issued per domain automatically by Traefik/ACME; no manual naming.
6. SFTP accounts
| Resource | Pattern | Example |
|---|---|---|
| SFTP user | sftp_<slug_underscored> |
sftp_example_com |
| Chroot path | tank/customers/<slug>/web |
— |
7. Deployment config (in deployments/)
deployments/
└── <slug>/
├── docker-compose.yml # rendered from a site-templates profile
├── .env.example # non-secret defaults / references
├── secrets.enc.yaml # SOPS/age-encrypted secrets (committed)
└── site.yaml # slug, profile, domains, options (source of truth)
site.yaml is the declarative source of truth the CLI reads/writes; everything
else is rendered from it.
8. Reserved words
Slugs may not be: proxy, platform, traefik, mariadb, forgejo,
monitoring, docker, customers, platform-infra, site-templates,
deployments, control-panel (avoids collisions with platform names).