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>
91 lines
3.6 KiB
Markdown
91 lines
3.6 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/<slug>/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/<slug>/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/<slug>/web` shared by both (web root).
|
|
- **DB:** optional — if requested, a `db_<slug>` + `u_<slug>` 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/<slug>/web` (WordPress core, themes, plugins,
|
|
uploads).
|
|
- **DB:** **required** — `db_<slug>` + `u_<slug>` 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
|
|
slug: example-com
|
|
profile: wordpress # static | redirect | custom-php | wordpress
|
|
domains: # first is primary → drives cert + slug
|
|
- 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.
|