# 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 `_net` network; only Traefik bridges to `proxy`. - Web root bind-mounted from `tank/customers//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//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//web` shared by both (web root). - **DB:** optional — if requested, a `db_` + `u_` 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//web` (WordPress core, themes, plugins, uploads). - **DB:** **required** — `db_` + `u_` 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.