Skip to content

DNS and TLS

Needs review

Needs review: The on-demand TLS ask check (vars_regexp on {query.domain} plus a file matcher on /tenant-env) could not be run through `caddy validate` while writing; validate it and issue a certificate for a test tenant.

Every tenant gets four host names, built from patterns in the API configuration (packages/tenancy/src/config.php). The development defaults are *.localhost; the production example sets these:

Variable (on api, as LARAVEL_…) Development default Production example
TENANCY_SCHEME http https
TENANCY_API_HOST {slug}.localhost {slug}.api.lms.example.com
TENANCY_FRONT_HOST {slug}.app.localhost {slug}.app.lms.example.com
TENANCY_ADMIN_HOST {slug}.admin.localhost {slug}.admin.lms.example.com
TENANCY_CONTENT_HOST {slug}.content.localhost {slug}.example-content.net, or same-site {slug}.content.lms.example.com
TENANCY_PLATFORM_HOSTS api.localhost,localhost,127.0.0.1,caddy,api api.lms.example.com,caddy,api,localhost,127.0.0.1

The patterns are applied when a tenant is created or synced; they end up in its env file as APP_URL, FRONTEND_URL, ADMIN_URL and CONTENT_ORIGIN. The front and admin find the tenant API from their own host with a matching rule:

  • web: ULAMS_TENANT_HOSTS={slug}.app.lms.example.com=>https://{slug}.api.lms.example.com
  • admin: REACT_APP_TENANT_API_HOST_PATTERN={slug}.admin.lms.example.com=>https://{slug}.api.lms.example.com

Slugs are 2 to 30 lowercase letters and digits starting with a letter; api, app, admin, www, storage, minio, ws, metrics, platform, default, test and postgres are reserved (TenantNaming). The fronts additionally never treat www, api, app, admin, stage or staging as a slug.

The API decides which tenant a request belongs to from X-Forwarded-Host, else Host (api/bootstrap/app.php), and answers 404 for hosts that are neither platform hosts nor provisioned tenants (TENANCY_ENFORCE_HOSTS). Keep php-fpm reachable only through the proxy, which sets those headers itself.

See Requirements. In short: explicit wildcard records for *.api, *.app and *.admin under the application domain, plus a wildcard on the content domain (separate, or under the application domain in the same-site mode).

The development Caddyfile serves plain HTTP. For production, pick one of these. The example Caddyfile ships the first.

Recommended Caddy obtains a certificate for a tenant host name the first time a browser connects to it, from Let’s Encrypt or ZeroSSL, with the stock caddy:2 image and no DNS API access.

The example restricts issuance with Caddy’s ask check, served by Caddy itself on an internal port: a name gets a certificate only if it has the form <slug>.api|app|admin.<domain> or <slug>.<content domain> and the env file .env.<slug>.api.<domain> exists. The api container copies those files to the tenant_env volume, which Caddy mounts read-only, so a newly created tenant becomes eligible within about five seconds. Unknown names get no certificate, so random sub-domains under your wildcard DNS cannot use up the CA’s rate limits.

{
email {$ACME_EMAIL}
on_demand_tls {
ask http://localhost:5555/check
}
}
*.app.{$ULAMS_DOMAIN} {
tls {
on_demand
}
reverse_proxy web:4321
}

Things to know:

  • The first request to a new tenant host waits for the ACME exchange (a few seconds).
  • Let’s Encrypt allows a limited number of certificates per registered domain per week; each tenant uses four. Many tenants created at once can hit the limit.
  • Fixed names (api., app., admin., platform., storage.) get ordinary certificates at start-up.

Moving an existing installation to new host names

Section titled “Moving an existing installation to new host names”

The TENANCY_*_HOST patterns only name new tenants. Each tenant row stores its api_host, front_host and admin_host, and ulams:tenant:sync-env rebuilds the env files from those stored values (only CONTENT_ORIGIN and the scheme follow the current configuration). Renaming the hosts of existing tenants therefore means updating those rows and the tenants’ global.frontURL setting; there is no command for it yet. Check the current values with php artisan ulams:tenant:list.