# 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 = `-` | 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`** = `-`, 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/ │ └── / # groups all of a customer's sites │ └── / │ └── web # web root ONLY (bind-mounted into the site) └── platform/ ├── docker # Docker data-root (images/layers/volumes) ├── mariadb # shared MariaDB datadir ├── db-backups//# 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///web@auto-YYYYMMDD-HHMM`. - Whole customer (recursive): `zfs snapshot -r tank/customers/@...`. ## 3. Docker resources (keyed by the flattened slug) | Resource | Pattern | Example | |----------|---------|---------| | Compose project | `` | `acme-shop` | | Container | `_` | `acme-shop_nginx`, `acme-shop_fpm` | | Per-site network | `_net` | `acme-shop_net` | | Shared edge network | `proxy` (fixed) | `proxy` | | Platform network | `platform` (fixed) | `platform` (MariaDB, monitoring) | | Named volume (if used) | `_` | `acme-shop_fpmtmp` | Traefik router/service labels also key off the slug: `traefik.http.routers..rule=Host(...)`. ## 4. Database (shared MariaDB) Hyphens in the slug become underscores for MySQL identifiers (`acme-shop` → `acme_shop`). | Resource | Pattern | Example | |----------|---------|---------| | Database | `db__` | `db_acme_shop` | | DB user | `u__` | `u_acme_shop` | | Grants | `ALL PRIVILEGES ON db__.*` 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_` | `sftp_acme` | | Chroot path | `tank/customers//` | 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/ └── / └── / ├── 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).