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>
134 lines
5.4 KiB
Markdown
134 lines
5.4 KiB
Markdown
# 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-]`, 2–32, starts with a letter | `acme` |
|
||
| `site` | One deployable unit, unique within its customer | `[a-z0-9-]`, 2–40, 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.com` → `acme-com`; `shop.acme.com` → `shop`).
|
||
- **`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-shop` → `acme_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:
|
||
|
||
```yaml
|
||
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).
|