Three bugs rega found on a laptop run of the container (2026-09-29): - /pb/_/ never rendered. admin.mjs proxied with fetch(), which returns the body decompressed, but forwarded PocketBase's Content-Encoding: gzip and Content-Length, so the browser tried to gunzip plain HTML. The proxy now drops both headers (for /pb and /api-prod). - PocketBase's own redirects left the mount: /pb/_ went to /_/, and /pb/_/index.html to http://127.0.0.1:8090/_/ (the internal address). Root-relative and upstream-absolute locations are put back under the mount: /pb/_/. - The research host's /embed-test set no MAP_APP_API_URL, so the maps asked that static host's own /api (404s; dashboard-plus-grid drew 489 nodes, not 1,399). The page now sets it from the manifest's runtime.apiBaseUrl before any bundle loads, as every generated page does. Checked both ways against an upstream that answers like PocketBase (gzipped HTML, redirects to /_/). The old proxy sends a gzip label on a plain body and /_/ locations; the new one sends a decodable body and /pb/_/. |
||
|---|---|---|
| .. | ||
| admin.mjs | ||
| apply-exclusions.sh | ||
| build-cdn.sh | ||
| cdn-cache.mjs | ||
| embed-test.html | ||
| entry.sh | ||
| pb-oidc.mjs | ||
| pb-prod-migrations.sh | ||
| pb-signin-check.html | ||
| public-exclusions.txt | ||
| README.md | ||
| vite.cloud.config.mjs | ||
Run dx-maps in the cloud: one shared dev server for a team
One container runs the Plugin Studio with hot reload for everyone. It clones the repo, pulls the
branch every few seconds, and Vite hot-reloads whatever changed, so every change an agent lands
on main shows up in everyone's browser within seconds. No rebuild; bun run build is only for
publishing.
With no OIDC settings it runs open, like bun run dev on a laptop. With OIDC settings a
sign-in page ("Sign in with your provider") sits in front of everything: the Studio, every map,
and the hot-reload websocket. Only people in the allowed groups get in.
Deploy
- Build the image from the repo-root
Dockerfile. It's a small bootstrap (Bun, git, and the oauth2-proxy binary); at start it clones the repo and runsentry.shfrom it, so the recipe updates with the repo. Point a container host at the repo, or paste the file into one: see Where it runs. - Set the environment below and expose the container's
PORTover HTTPS. - Give it about 2 GB of memory (Vite plus the dependency install).
| Variable | Needed | Meaning |
|---|---|---|
REPO_URL |
yes | the dx-maps git URL to clone |
BRANCH |
no (main) |
the branch to follow |
PULL_INTERVAL |
no (15) |
seconds between pulls; 0 never pulls (for a checkout mounted from your machine: a pull resets it hard) |
GIT_TOKEN |
private repos | a read token for the clone and pulls |
PORT |
no (5173) |
the port the container serves on |
HMR_CLIENT_PORT, HMR_PROTOCOL |
behind HTTPS | 443 and wss, so browsers reach the hot-reload websocket |
Where it runs
The container needs a Dockerfile build, a persistent volume at /data and about 2.5 GB of memory.
| Platform | Build | /data |
|---|---|---|
| Coolify | the "Dockerfile" build pack (default location /Dockerfile) |
Persistent Storage, mount path /data |
| Dokploy | Build Type "Dockerfile" (path Dockerfile) |
Advanced, Volumes: mount path /data |
| Railway | finds the root Dockerfile by itself; set PORT=5173 |
add a volume at /data. Railway deploys from GitHub, a local directory or an image |
| A server with Docker | docker compose up -d --build, with the root compose.yaml |
the compose file's dx-data volume |
| Kubernetes | build the image in CI (BuildKit, Buildah) and push it to a registry | a PersistentVolumeClaim at /data |
| DigitalOcean App Platform | not suitable | it has no persistent volumes, so PocketBase's data would be lost on every deploy |
There is no Railpack file. Railpack is Railway's default builder (Coolify and Dokploy offer it too), but it can't reuse this Dockerfile; the container is defined here, once.
Sign-in (optional)
Set all of these to turn the gate on. Register a confidential client at your OIDC provider
with the redirect URL https://<your-host>/oauth2/callback; PKCE (S256) is used as well.
| Variable | Meaning |
|---|---|
OIDC_ISSUER |
the provider's issuer URL (its /.well-known/openid-configuration must resolve) |
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET |
the registered client |
OIDC_REDIRECT_URL |
https://<your-host>/oauth2/callback |
OIDC_ALLOWED_GROUPS |
comma-separated groups allowed in; required (the server refuses to start without it, because many providers allow open sign-up) |
OIDC_DISPLAY_NAME |
the name on the button, e.g. "Sign in with Acme" |
OIDC_GROUPS_CLAIM |
the token claim holding groups (default groups) |
OIDC_COOKIE_SECRET |
32 characters; keeps people signed in across restarts (random if unset) |
Secrets reach oauth2-proxy through its environment, never its command line.
Backend and production (v1, experimental)
With the sign-in on, the container also runs PocketBase and can publish a production build:
| Route | Who | What |
|---|---|---|
/ |
signed-in | the dev server (Plugin Studio, hot reload) |
/pb/ |
signed-in (its /_/ dashboard: OIDC_ADMIN_GROUPS only). With PB_OIDC_CLIENT_SECRET, its /api/ is public and PocketBase signs people in itself (see below); _superusers and system collections stay admin-only |
PocketBase dev, the team's shared backend |
/api-prod/api/ |
public | PocketBase prod's API; its dashboard is never exposed |
/cdn/dx-maps/ |
public | the last published build: manifest.json at a stable URL, CORS open for embeds |
POST /admin/build · GET /admin/status |
OIDC_ADMIN_GROUPS only |
build and publish the pulled commit |
A build runs one at a time (a second gets 409), on a snapshot of the pulled commit, with Node's heap
capped. It removes everything in public-exclusions.txt before
building, checks the output again afterwards, and refuses to publish if anything excluded is still
there. Only then does it swap the new build in.
| Variable | Meaning |
|---|---|
OIDC_ADMIN_GROUPS |
comma-separated groups allowed to build and to open PocketBase's dashboard |
PB_SUPERUSER_EMAIL, PB_SUPERUSER_PASSWORD |
PocketBase's superuser, set on start in both instances |
PB_OIDC_CLIENT_SECRET (+ PB_OIDC_CLIENT_ID, default dx-maps-pb-dev) |
turns pb-dev into the team backend: laptops use it directly (DX_PB_URL=https://<host>/pb), and people sign in to PocketBase through the same OIDC provider, with its own client whose redirect URI is https://<host>/pb/api/oauth2-redirect. Only members of OIDC_ALLOWED_GROUPS get in (a sign-in hook baked into the image and pinned; the repo's own hooks never run). Password sign-in is off, new collections are superuser-only until a migration opens them, and data never leaves /data |
PRODUCTION_BRANCH |
the branch /cdn/ is built from (default production); the dev server keeps following BRANCH. Promote with git push origin main:production |
CDN_AUTOBUILD_MINUTES |
rebuild /cdn/ by itself once PRODUCTION_BRANCH has held a new commit this long (default 2; 0 = only the admin button) |
SUBDOMAIN_APPS |
publish plugins as their own PUBLIC sites: host=bundle/profile, comma-separated, e.g. research-gallery.example.org=doxa-maps-spa/doxa-maps-spa. Served from the /cdn build by a separate server on SUBDOMAIN_PORT (default 5176) that answers only those hosts and only with published files; point each host's proxy route at that port. No sign-in is skipped anywhere else |
PUBLIC_API_BASE |
the public API URL written into the build's runtime-config.js (e.g. https://<host>/api-prod) |
BUILD_MAX_OLD_SPACE_MB |
Node heap cap for a build (default 1536) |
RUDDER_ENGINE |
1 runs Rudder's voice-to-text engine here, fed by the browser's microphone (about 1 GB more; its runtime and model, ~742 MB, are baked into the image at /opt/rudder, and /data/rudder wins when it holds a model). The site serves at once; Rudder shows "starting" until its engine is built, then the dev server restarts once to start it. Give the container at least 2 vCPUs, or transcription falls behind speech |
RUDDER_ALLOW_HOST |
every public host the engine accepts, comma-separated (it refuses others, so list each domain the container answers on); defaults to Coolify's COOLIFY_FQDN, else the host in OIDC_REDIRECT_URL |
Mount a persistent volume at /data (PocketBase data and published builds). Memory measured on
the running container: about 0.45 GB idle, up to about 1.5 GB more during a build, so give it
2.5 GB (3.5 GB with RUDDER_ENGINE=1).