platform/docs/03-naming-conventions.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

5.4 KiB
Raw Permalink Blame History

03 — Naming & Conventions

Consistent, predictable names are what make CLI-driven provisioning and scripted backup/restore reliable. The model has two levels: a customer (owner) may own many sites (deployable units). Isolation, containers, and databases live at the site level; grouping (SFTP login, billing, bulk backup/delete) lives at the customer level.

1. Identifiers

Id Scope Charset Example
customer Owner / billing entity [a-z0-9-], 232, starts with a letter acme
site One deployable unit, unique within its customer [a-z0-9-], 240, starts with a letter shop
slug Globally-unique flattened id = <customer>-<site> derived acme-shop
  • customer is chosen once (short, stable — usually the company name).
  • site identifies an app within that customer. Derive from the primary domain or set explicitly (acme.comacme-com; shop.acme.comshop).
  • slug = <customer>-<site>, the join key for Docker and database names. Neither customer nor site changes for the life of the resource (renaming = new id + migration).

Same app vs separate app (subdomains)

  • A subdomain that serves the same application (www.acme.com + acme.com, vanity domains) is an alias — extra entries in one site's domains: list, one web root, one cert. Not a new site.
  • A subdomain that is a separate application (shop.acme.com webshop vs acme.com marketing) is a separate site under the same customer — its own slug, web root, DB, containers, and network. Isolated from the customer's other sites; grouped under the customer for SFTP/backup/billing.

2. ZFS datasets — nested customer → site

tank/
├── customers/
│   └── <customer>/                 # groups all of a customer's sites
│       └── <site>/
│           └── web                 # web root ONLY (bind-mounted into the site)
└── platform/
    ├── docker                      # Docker data-root (images/layers/volumes)
    ├── mariadb                     # shared MariaDB datadir
    ├── db-backups/<customer>/<site># automysqlbackup output per site
    ├── traefik                     # ACME store + dynamic config
    ├── forgejo                     # Git + registry data
    └── monitoring                  # Prometheus/Loki data

Example for customer acme:

tank/customers/acme/acme-com/web    # WordPress marketing site
tank/customers/acme/shop/web        # custom-PHP webshop (separate app + DB)
tank/customers/acme/blog/web        # another WordPress

Snapshots:

  • Per site: tank/customers/<customer>/<site>/web@auto-YYYYMMDD-HHMM.
  • Whole customer (recursive): zfs snapshot -r tank/customers/<customer>@....

3. Docker resources (keyed by the flattened slug)

Resource Pattern Example
Compose project <slug> acme-shop
Container <slug>_<role> acme-shop_nginx, acme-shop_fpm
Per-site network <slug>_net acme-shop_net
Shared edge network proxy (fixed) proxy
Platform network platform (fixed) platform (MariaDB, monitoring)
Named volume (if used) <slug>_<purpose> acme-shop_fpmtmp

Traefik router/service labels also key off the slug: traefik.http.routers.<slug>.rule=Host(...).

4. Database (shared MariaDB)

Hyphens in the slug become underscores for MySQL identifiers (acme-shopacme_shop).

Resource Pattern Example
Database db_<customer>_<site> db_acme_shop
DB user u_<customer>_<site> u_acme_shop
Grants ALL PRIVILEGES ON db_<customer>_<site>.* to that user only

Passwords are generated, stored encrypted (SOPS/age), never reused across sites.

5. Domains & TLS

  • The site's primary domain drives the default site id; additional aliases live in the site's domains: list and are added to the Traefik Host rule.
  • Certificates are issued per domain automatically by Traefik/ACME.

6. SFTP accounts — per customer

One login per customer, chrooted to the customer directory so they see all their sites as sub-folders.

Resource Pattern Example
SFTP user sftp_<customer> sftp_acme
Chroot path tank/customers/<customer>/ sees acme-com/web, shop/web, …

(A future option: per-site SFTP users for customers who want to delegate access to a single site. Default is one per customer.)

7. Deployment config (in deployments/) — mirrors ZFS

deployments/
└── <customer>/
    └── <site>/
        ├── site.yaml            # declarative source of truth
        ├── 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 is the source of truth the CLI reads/writes; everything else is rendered from it. It carries both ids:

customer: acme
site: shop
profile: custom-php
domains: [shop.acme.com]

8. Reserved words

customer and site ids may not be: proxy, platform, traefik, mariadb, forgejo, monitoring, docker, customers, platform-infra, site-templates, deployments, control-panel (avoids collisions with platform names).