PenCMS works on http://localhost for local curl and same-machine agents. Stock Custom Connectors and browser OAuth consent expect a stable HTTPS origin whose scheme/host match JWT_ISSUER and MCP_RESOURCE_URL.
You do not need a public cloud for that. A common pattern (same idea as SilverBullet’s LAN install): run PenCMS on a machine on your LAN, terminate TLS at a reverse proxy, and point clients at that HTTPS URL.
[ Laptop / phone / agent ]
| https://pencms.lan (or https://192.168.x.x)
v
[ Reverse proxy: Caddy / Nginx / Apache ] ← TLS + optional HSTS
|
+--> PHP admin (frontend, loopback :8009)
+--> FastAPI backend (loopback :8008)
-
Pick a hostname clients will use, e.g.
pencms.lanor a fixed LAN IP. Put it in/etc/hosts(or your router DNS) on every client that needs the admin UI or OAuth redirect. -
Issue a locally trusted cert with
mkcert(or your own CA):mkcert -install mkcert pencms.lan 192.168.1.50
Point the reverse proxy at the generated
.pem/-key.pem. -
Proxy
/(or admin paths) to the PHP frontend and/api,/.well-known,/oauth,/llms.txtto the Python backend (or proxy everything to PHP’srouter.phpif it already forwards/api/*). -
Set env on the backend to the same HTTPS origin clients type in the browser:
export JWT_ISSUER=https://pencms.lan export MCP_RESOURCE_URL=https://pencms.lan/api/mcp export CORS_ALLOW_ORIGINS=https://pencms.lan
Scheme and host must match exactly (no
http://leftovers, no trailing slash on issuer). -
Smoke: open
https://pencms.lan, complete OAuth consent once, then point Cursor/Claude athttps://pencms.lan/api/mcp.
Prefer this on a home/lab box that already has nginx and PHP. Docker Compose (deploy_compose.md) is the VPS/public-HTTPS path; it does not yet run PHP and would fight an existing nginx on :80/:443.
Drop-in files live in deploy/lan/:
| File | Role |
|---|---|
nginx-pencms.conf |
TLS terminator; /api, /.well-known, /oauth, /llms.txt → FastAPI :8008; /fonts/, /assets/vendor/, /assets/fonts/, /admin/css/ → disk; /admin/js/ Core-on-disk then PHP (PENCMS_PRO_ADMIN); everything else / → PHP :8009 |
pencms-api.service |
uvicorn on 127.0.0.1:8008 (--reload for git-pull/edit LAN boxes; omit on a hands-off VPS). --reload drops in-memory MCP sessions — echo a fresh Mcp-Session-Id after the API restarts, or omit --reload if agents keep long Streamable HTTP sessions. REST /api/v1/mcp/* is unaffected. |
pencms-php.service |
php -S 127.0.0.1:8009 + router.php (PHP_CLI_SERVER_WORKERS=8) |
pencms.env.example |
JWT_ISSUER / MCP_RESOURCE_URL / CORS_ALLOW_ORIGINS must be the HTTPS origin clients type. Optional PYTHONPATH + PENCMS_PRO_ADMIN load a sibling Pro overlay. |
Copy pencms.env.example → pencms.env (mode 0600), set a long JWT_SECRET, then either:
- User systemd (graphical login or
loginctl enable-linger): drop the two.servicefiles in~/.config/systemd/user/, omitUser=/Group=,WantedBy=default.target. - System systemd: install the units as-is under
/etc/systemd/system/.
After editing a unit file or pencms.env, recopy the unit into ~/.config/systemd/user/ (or /etc/systemd/system/), then daemon-reload and restart the service. uvicorn --reload watches Python source only — it does not pick up unit-file PATH, EnvironmentFile, or ExecStart changes.
Enable the nginx site, nginx -t, reload. First browser visit: https://$HOST/admin/setup.php (bootstrap admin), then mint a site-scoped agent key.
Pin mcp>=1.12.0,<2 — fastapi-mcp==0.4.0 crashes on MCP SDK 2.0.
Edition is overlay presence, not a config.ini flag. Core stays Core until the API can import pencms_pro and PHP can serve Users/Sites from the overlay admin tree. See editions.md.
-
Check out
pencms-proas a sibling of Core (example:/home/user/pencms+/home/user/pencms-pro). -
In
pencms.env(not bashexport— systemdEnvironmentFileis not a shell), set:PYTHONPATH=/home/user/pencms-pro PENCMS_PRO_ADMIN=/home/user/pencms-pro/frontend-php/src/admin
Both services must load that file. The API unit already does. The PHP unit must list
EnvironmentFile=too, or Users/Sites PHP will 404 while/api/configsays"edition":"pro". -
Restart both units (
systemctl --user restart pencms-api pencms-phpor the system equivalents).init_profailures fail API boot on purpose; a missingPYTHONPATHis a silent Core boot (ImportErrorswallowed). -
Recopy
nginx-pencms.confinto the live nginx site andnginx -t+ reload./admin/js/is served from Core disk first; overlay scripts (users.js,settings-sites.js) fall through to PHP. Skipping this step 404s those scripts and Alpine reportsusersAdmin/sitesSettingsis not defined. -
Smoke:
GET /api/config→"edition":"pro"; hard-refresh admin; sidebar Users and Sites;GET /admin/js/users.jsis 200.
Content lives under content/sites/{id}/ after first-boot migration. Use one MCP connector URL (MCP_RESOURCE_URL) for the whole install; bind agents with named, site-scoped keys (PHP Settings → Sites / AI → Agent Keys). Details: mcp_guide.md, deploy_compose.md.
- Cloud-hosted Custom Connectors (e.g. some Claude connector flows) must reach your URL from their network. Pure LAN HTTPS is enough for browsers and agents on the same network; for off-LAN agents use a VPS, Tailscale/funnel, or a tunnel — still set
JWT_ISSUER/MCP_RESOURCE_URLto that reachable HTTPS URL.
Theme Customize inspect needs Chromium on the API host (same process as FastAPI) and PENCMS_PREVIEW_BASE_URL set to the PHP origin that process can GET /blog/ from (never the API port).
pip install playwright && playwright install --only-shell
export PENCMS_PREVIEW_BASE_URL=http://127.0.0.1:8009Docker Compose uses a separate image target (PENCMS_API_TARGET=inspect): deploy_compose.md.
- VPS / public HTTPS via Docker Compose + Caddy:
deploy_compose.md - MCP operator notes:
mcp_guide.md(deployment + smoke checklist) - Agent-owned site (sponsor + daily editor):
agent-owned-site.md - Product direction:
product_thesis.md - Longer Secure Context / mkcert walkthrough (same TLS idea as SilverBullet / the Rezilienz prototype):
securing_local_installations.mdif you still keep a copy of that tree.