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>
92 lines
3.8 KiB
Markdown
92 lines
3.8 KiB
Markdown
# 04 — Site Profiles
|
|
|
|
A **profile** is a standard site stack with a compose template in
|
|
`site-templates/`. The provisioning CLI renders a profile with a site's slug,
|
|
domains, and options. Four profiles cover all current workloads.
|
|
|
|
Common to every profile:
|
|
- Traefik labels for routing + TLS (Host rule from domains, ACME resolver).
|
|
- Joined to the site's own `<slug>_net` network; only Traefik bridges to `proxy`.
|
|
- Web root bind-mounted from `tank/customers/<customer>/<site>/web`.
|
|
- Per-project CPU/memory limits.
|
|
- Containers run non-root; rootfs read-only where the profile allows.
|
|
|
|
| Profile | Web server | PHP | Database | Typical use |
|
|
|---------|-----------|-----|----------|-------------|
|
|
| `static` | nginx (or Traefik direct) | — | — | HTML/JS sites, landing pages |
|
|
| `redirect` | Traefik router / tiny nginx | — | — | Domain redirects |
|
|
| `custom-php` | nginx | php-fpm | optional | Bespoke PHP apps |
|
|
| `wordpress` | nginx | php-fpm | required | WordPress sites |
|
|
|
|
---
|
|
|
|
## `static`
|
|
|
|
- **Containers:** one nginx serving the web root read-only. For very simple
|
|
cases, Traefik can serve files directly with no container.
|
|
- **Volumes:** `tank/customers/<customer>/<site>/web` → `/usr/share/nginx/html` (read-only).
|
|
- **DB:** none.
|
|
- **Notes:** cheapest profile; near-zero backup incrementals when unchanged.
|
|
|
|
## `redirect`
|
|
|
|
- **Containers:** none preferred — implemented as a Traefik router rule with a
|
|
redirect middleware (`RedirectRegex`/`RedirectScheme`). A tiny nginx is the
|
|
fallback if complex rewrite logic is needed.
|
|
- **Volumes:** none.
|
|
- **DB:** none.
|
|
- **Config:** target URL(s) and redirect type (301/302), preserve-path flag —
|
|
all in `site.yaml`.
|
|
|
|
## `custom-php`
|
|
|
|
- **Containers:**
|
|
- `nginx` — serves static assets, proxies `.php` to fpm over FastCGI.
|
|
- `php-fpm` — non-root; only the web root (and a small tmp) writable.
|
|
- **Volumes:** `tank/customers/<customer>/<site>/web` shared by both (web root).
|
|
- **DB:** optional — if requested, a `db_<customer>_<site>` + `u_<customer>_<site>` on shared MariaDB;
|
|
credentials injected via env from `secrets.enc.yaml`.
|
|
- **Options:** PHP version (pinned image tag), extensions, `php.ini` overrides,
|
|
cron (via a scheduled fpm exec) if needed.
|
|
|
|
## `wordpress`
|
|
|
|
- **Containers:** same nginx + php-fpm pair as `custom-php`, using a WordPress
|
|
base image (or WP installed into the web root on first provision).
|
|
- **Volumes:** `tank/customers/<customer>/<site>/web` (WordPress core, themes, plugins,
|
|
uploads).
|
|
- **DB:** **required** — `db_<customer>_<site>` + `u_<customer>_<site>` on shared MariaDB.
|
|
- **Hardening baked in:**
|
|
- php-fpm non-root; `wp-content/uploads` writable, PHP execution denied there
|
|
(nginx rule) to blunt upload-based RCE.
|
|
- Read-only rootfs where WordPress tolerates it; XML-RPC restricted; sensible
|
|
security headers via Traefik middleware.
|
|
- WP-CLI available for provisioning/maintenance (install, update, search-
|
|
replace on domain migration).
|
|
- **Options:** PHP version, multisite flag, alias domains, initial admin user.
|
|
|
|
---
|
|
|
|
## Profile inputs (from `site.yaml`)
|
|
|
|
Every profile is rendered from the same declarative fields; unused fields are
|
|
ignored per profile:
|
|
|
|
```yaml
|
|
customer: acme # owner id
|
|
site: acme-com # site id, unique within the customer
|
|
profile: wordpress # static | redirect | custom-php | wordpress
|
|
domains: # first is primary → drives cert + default site id
|
|
- example.com
|
|
- www.example.com
|
|
php_version: "8.3" # custom-php / wordpress
|
|
database: true # custom-php (wordpress forces true)
|
|
redirect_to: null # redirect profile only
|
|
resources:
|
|
cpu: "1.0"
|
|
memory: "512m"
|
|
```
|
|
|
|
Adding a profile = adding a template in `site-templates/` and a case in the CLI
|
|
renderer. Keep the set small; special cases are options on a profile, not new
|
|
profiles.
|