services/dph-am-stack.md
Table of Contents
dph.am Stack — Master Reference
Start here for anything involving *.dph.am (or the handful of qwestly.co/other domains riding the same nginx box). This doc ties together DNS, the router, nginx, the auth layer, the PWA-injection pattern, and certbot into one picture, plus a step-by-step runbook for adding or removing an app. The three detailed docs below cover their own slice in depth — read this one first to know which of them to open.
| Doc | Covers |
|---|---|
../hardware/mac-studio/services/nginx.md |
The nginx server itself — Homebrew install, running as a root LaunchDaemon, default_server fallback, migration history/gotchas |
../hardware/mac-studio/services/nginx-auth-service.md |
The auth_request login wall, per-app backend list, OpenClaw's trusted-proxy integration |
../hardware/mac-studio/services/nginx-pwa-injection.md |
Turning an app into an installable PWA + injecting custom scripts/styles via sub_filter, served from ~/Work/pwa/<app>/ |
| This doc | How all of the above fit together end-to-end, DNS/router/certbot, and the add/remove-app runbook |
The full request path
graph TD
DNS["Wildcard DNS
*.dph.am → home public IP"] --> Router["Router
forwards 22, 80, 443"]
Router --> Nginx["nginx (root LaunchDaemon)
Mac Studio 192.168.1.189
/opt/homebrew/etc/nginx"]
Nginx --> Match{"server_name
match?"}
Match -->|no match| Fallback["_localhost.conf default_server
serves ~/Work"]
Match -->|matched site| Auth{"auth_request
configured?"}
Auth -->|yes| AuthSvc["nginx-auth-service :8085
cookie/session check"]
AuthSvc -->|valid| Inject["sub_filter injects
manifest/script/style
from ~/Work/pwa/<app>/ via alias"]
AuthSvc -->|invalid| Login["redirect to /login"]
Auth -->|no, app has own auth
e.g. c-de.dph.am| Inject
Inject --> Backend["backend app
(OpenCode, pi-webui, ttyd, etc.)"]
style DNS fill:#2d5a27,color:#fff
style Router fill:#4a4a1a,color:#fff
style Nginx fill:#1a4a7a,color:#fff
style AuthSvc fill:#6b3fa0,color:#fff
style Backend fill:#8b4513,color:#fff
Why this shape exists: wildcard DNS means any <anything>.dph.am reaches this Mac — there's no DNS gatekeeping per-app. The gatekeeping happens entirely at nginx: a site either gets its own sites-enabled/<name>.dph.am.conf (deliberately exposed, usually behind auth_request) or it doesn't, in which case it silently falls through to the ~/Work file listing. Wildcard reach + selective nginx config, rather than DNS-level allowlisting, is the actual security boundary.
The four pieces, briefly
- DNS:
*.dph.amwildcard A record → home's public IP. No per-subdomain DNS entries needed — adding a new<name>.dph.amapp never requires a DNS change, only an nginx conf. - Router: ports 22 (SSH), 80 (HTTP, also used for certbot's HTTP-01 challenges), 443 (HTTPS) forwarded to
192.168.1.189(Mac Studio). This one box is the single ingress point for everything. - nginx: runs as a root LaunchDaemon (
sh.brew.nginx, not the default Homebrew user-level agent) so it can read Certbot's root-only certs and bind ports 80/443.sites-enabled/*.confis the actual routing table — one file per domain. See../hardware/mac-studio/services/nginx.md. - certbot: issues one individual Let's Encrypt cert per subdomain via the nginx plugin (HTTP-01 challenge, not wildcard/DNS-01) — currently ~33 certs. See the Certbot section below for the Apple-Silicon path bug and the renewal LaunchDaemon.
Layered on top of nginx routing, two optional, independent behaviors per site:
- Perimeter auth (
auth_request→ nginx-auth-service) —../hardware/mac-studio/services/nginx-auth-service.md - PWA injection (
sub_filter+ static assets from~/Work/pwa/<app>/) —../hardware/mac-studio/services/nginx-pwa-injection.md
Both are opt-in per sites-enabled/*.conf file — a site can have neither, either, or both.
Certbot specifics
- Authenticator:
nginxplugin, HTTP-01. Not a wildcard cert — every subdomain gets issued and renewed individually.authenticator = nginx/installer = nginxin each/etc/letsencrypt/renewal/<domain>.conf. - Account: already registered (
account = 7d56dba1eeffab226462bb144eaedc0cin each renewal conf) — never needcertbot registeragain on this box. - 🐛 Apple Silicon path bug (fixed 2026-09-20): the certbot nginx plugin defaults to
/usr/local/etc/nginx/nginx.confand binarynginxon$PATH— the Intel Homebrew layout. On this Apple Silicon Mac, nginx actually lives at/opt/homebrew/etc/nginx/nginx.conf//opt/homebrew/opt/nginx/bin/nginx. Without a fix, every single renewal silently fails (nginx: [emerg] open() "/usr/local/etc/nginx/nginx.conf" failed) — discovered when0.dph.am's cert was 4 days from expiry with zero renewal infrastructure in place. Fixed permanently via/etc/letsencrypt/cli.ini:
This file is read by everynginx-server-root = /opt/homebrew/etc/nginx nginx-ctl = /opt/homebrew/opt/nginx/bin/nginxcertbotinvocation (manual or scheduled) as long as it runs on this machine — no per-command flags needed. - Auto-renewal LaunchDaemon:
/Library/LaunchDaemons/com.certbot.renew.plist(root,StartCalendarIntervalat 03:30 and 15:30 daily), runs:
Thecertbot renew --quiet --deploy-hook "/opt/homebrew/opt/nginx/bin/nginx -s reload"--deploy-hookonly fires nginx reload on domains that actually renewed (Let's Encrypt only renews certs within ~30 days of expiry), so this is safe to run twice daily indefinitely. This LaunchDaemon did not exist before 2026-09-20 — the old machine presumably had its own renewal setup that didn't carry over in the migration; verify this exists after any future migration to a new box.- Verify it's registered:
sudo launchctl print system/com.certbot.renew | grep -i state - Logs:
/var/log/certbot-renew.log - Manually test without touching real rate limits:
sudo certbot renew --dry-run
- Verify it's registered:
Runbook: adding a new app
Wildcard DNS means no DNS step is ever needed for a new <name>.dph.am. Everything happens in nginx + certbot + (optionally) ~/Work/pwa/.
- Decide the shape: does it need perimeter auth (almost always yes, unless the backend has its own real auth — see
c-de.dph.amas the precedent for "no")? Does it need PWA treatment (installable icon, injected UX script/style)? - Get a minimal HTTP-only conf running first (certbot's nginx plugin needs to see a
server_nameblock to attach to):# /opt/homebrew/etc/nginx/sites-enabled/<name>.dph.am.conf server { listen 80; server_name <name>.dph.am; location / { return 404; } }sudo nginx -t && sudo nginx -s reload - Issue the cert — this single command both obtains the cert and rewrites the conf file to add the
listen 443 ssl, cert paths, and an HTTP→HTTPS redirect block automatically (same pattern as every existing site):sudo certbot --nginx -d <name>.dph.am - Flesh out the conf — edit the now-certbot-modified file to add, inside the
location /block for port 443:- If backend needs perimeter auth: copy the
auth_request//login//logout/= /authblocks from an existing protected site (e.g.dash.dph.am.confis a clean minimal example) and the actualproxy_pass http://127.0.0.1:<port>;— use127.0.0.1, not the LAN IP, since nginx and every backend now run on the same box (see themax.dph.amwrite-up in../hardware/mac-studio/services/nginx-auth-service.mdfor why LAN-IP self-reference caused a real outage with OpenClaw's trusted-proxy check). - If it needs PWA treatment:
mkdir ~/Work/pwa/<name>/, addmanifest.json/icon/etc., then addlocation = /manifest.json { alias /Users/dominick/Work/pwa/<name>/manifest.json; }(orlocation /_static/ { alias /Users/dominick/Work/pwa/<name>/; }for a directory bucket) plus asub_filter '<head>' '<head><link rel="manifest" ...>...';+proxy_set_header Accept-Encoding "";in the main location block. Full detail in../hardware/mac-studio/services/nginx-pwa-injection.md.
- If backend needs perimeter auth: copy the
- Test and reload:
sudo nginx -t && sudo nginx -s reload - Verify:
curl -sI https://<name>.dph.am/and confirm the app loads (and the login wall triggers, if applicable).
Runbook: removing an app
sudo rm /opt/homebrew/etc/nginx/sites-enabled/<name>.dph.am.conf
sudo certbot delete --cert-name <name>.dph.am --non-interactive
sudo nginx -t && sudo nginx -s reload
If it had PWA assets: rm -rf ~/Work/pwa/<pwa-dir-name>/ (the PWA directory name doesn't always match the domain — e.g. rsx.dph.am used ~/Work/pwa/reasonix/, 0.dph.am/c-de.dph.am both used ~/Work/pwa/opencode/. Check ../hardware/mac-studio/services/nginx-pwa-injection.md's per-app table, or just grep -r <name> /opt/homebrew/etc/nginx/sites-enabled/ before deleting a shared PWA dir, since a couple of domains intentionally reuse another app's PWA folder).
2026-09-20 cleanup, for reference: removed supabase.dph.am, supabase-api.dph.am, rsx.dph.am, printer.dph.am, flowise.qwestly.co, excalidraw-collab.dph.am, dsh.dph.am, draw.dph.am, ccli.dph.am, candidate-dom-local.qwestly.co, ai.dph.am — all unused, along with their certs and (for rsx/dsh) their PWA folders. Down from ~44 certs to 33, and 37 files in sites-enabled/ (including _localhost.conf and default.conf, which aren't per-app sites).
Known gaps / open items (consolidated from the three detail docs)
- Extract the repeated
auth_request/login/logoutblock into a shared nginxincludesnippet (currently duplicated across 7 files). - Pull the two hardcoded secrets (0.dph.am's Basic Auth header, formerly rsx.dph.am's token — now removed) out of plaintext conf files.
- Add
limit_reqrate limiting on/login— these hosts are directly internet-facing via the wildcard. - Confirm OpenClaw's
trusted-proxyconfig stays correct if nginx or OpenClaw ever move machines again (loopback-only trust,allowLoopback: true,allowUsers: ["dominick"]— see../hardware/mac-studio/services/nginx-auth-service.md).