Cloud container: main is the default production branch; the embed address and open mode are documented

- PRODUCTION_BRANCH defaulted to `production`, a branch this repo does not have (development is the
  table, main is production). compose.yaml and every deployment already set main; a platform deploy
  that left it unset watched a missing branch and /cdn never built. Default main in build-cdn.sh,
  pb-prod-migrations.sh and admin.mjs, and in the README.
- EMBED_BASE_URL was missing from the settings table. Unset, the published snippets stay relative.
- Open mode is for a laptop or a private network: it has none of the public mode's guards. Said so.
- compose.yaml reads the repo-root .env, the same file bun run dev reads, so laptop-only settings
  would reach the container. Said so in compose.yaml and the platform table.
This commit is contained in:
Doxa Maps 2026-09-29 08:35:02 +00:00
parent 6289d3b969
commit f29df44140
5 changed files with 15 additions and 11 deletions

View file

@ -2,6 +2,8 @@
# docker compose up -d --build
# Settings come from a .env file next to this one (never committed): REPO_URL is required, GIT_TOKEN
# for a private repo, and the OIDC_* settings for the sign-in; every setting is in deployment/cloud/README.md.
# It is the same .env that `bun run dev` reads, and all of it reaches the container: on a server, keep
# laptop-only settings (DX_PB_URL, DX_SYNC_*) out of it.
services:
dx-maps:
build: .

View file

@ -7,7 +7,9 @@ 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.
and the hot-reload websocket. Only people in the allowed groups get in. Open is for a laptop or a
private network only: it has none of the public mode's guards. On a public host, use the sign-in or
[`ACCESS=public`](#public-studio-accesspublic).
## Deploy
@ -37,7 +39,7 @@ The container needs a Dockerfile build, a persistent volume at `/data` and about
| 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`](../../compose.yaml) | the compose file's `dx-data` volume |
| A server with Docker | `docker compose up -d --build`, with the root [`compose.yaml`](../../compose.yaml). It reads the repo-root `.env`, the same file `bun run dev` reads, so on a server keep only the container's settings there | 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 |
@ -108,7 +110,8 @@ there. Only then does it swap the new build in.
| `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` |
| `PRODUCTION_BRANCH` | the branch `/cdn/` is built from (default `main`); the dev server keeps following `BRANCH`. It moves only on a publish |
| `EMBED_BASE_URL` | the address the published `dx-maps/` folder sits under, so the manifests' copy-paste snippets are absolute: `https://<host>/cdn` (it is served at `/cdn/dx-maps/`), or a research subdomain's `https://research.<host>` (served at `/dx-maps/`). Unset, the snippets stay relative (`./…`) and other sites can't paste them |
| `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`) |

View file

@ -89,11 +89,10 @@ const publishedCommit = () => (existsSync('/data/cdn/COMMIT') ? readFileSync('/d
// The production PocketBase's schema: the snapshot its migrations came from (pb-prod-migrations.sh), or 'empty'.
const prodSchema = () => { try { return basename(realpathSync('/data/pb-prod-migrations/current')) } catch { return null } }
// AUTO-BUILD: the public build follows PRODUCTION_BRANCH by itself. main is the shared sandbox; production
// moves only when someone promotes (git push origin main:production), so rebuilding on every move is
// the point. Debounced: build once the branch has held the same commit for CDN_AUTOBUILD_MINUTES
// AUTO-BUILD: the public build follows PRODUCTION_BRANCH by itself (default main). The dev server follows
// BRANCH, the shared table; main moves only on a publish, so rebuilding on every move is the point. Debounced: build once the branch has held the same commit for CDN_AUTOBUILD_MINUTES
// (default 2; 0 = off, admin button only). A commit whose build failed is not retried automatically.
const PROD = env.PRODUCTION_BRANCH || 'production'
const PROD = env.PRODUCTION_BRANCH || 'main'
const AUTO_MIN = env.CDN_AUTOBUILD_MINUTES === undefined || env.CDN_AUTOBUILD_MINUTES === '' ? 2 : Number(env.CDN_AUTOBUILD_MINUTES)
const watch = { seen: null, since: 0, failed: null, error: null }
async function checkProduction() {

View file

@ -1,7 +1,7 @@
#!/bin/bash
# build-cdn.sh — one public build of the PRODUCTION branch into /data/cdn, one at a time.
# main is the shared sandbox the dev server follows; the public build comes only from
# PRODUCTION_BRANCH (default production), which moves when someone promotes main to it.
# The dev server follows BRANCH (the team's shared table, e.g. development); the public build comes only
# from PRODUCTION_BRANCH (default main), which moves only on a publish.
# fetch that branch into its own ref -> snapshot it -> apply ITS public exclusions ->
# the build steps WITHOUT the gates (memory-capped) -> check the output -> the exclusion check again
# (refuses the swap) -> atomic swap of /data/cdn/current.
@ -14,7 +14,7 @@ exec 9>/tmp/build-cdn.lock
flock -n 9 || { echo "another build is running"; exit 75; }
echo $$ > /tmp/build-cdn.pid
cd /app/repo
PB="${PRODUCTION_BRANCH:-production}"
PB="${PRODUCTION_BRANCH:-main}"
# Its own ref, and no FETCH_HEAD: the dev server's pull loop uses FETCH_HEAD for main at the same time.
git fetch -q --no-write-fetch-head --depth 1 origin "+refs/heads/$PB:refs/cdn/$PB" \
|| { echo "cannot fetch branch '$PB' (create it with: git push origin main:$PB)"; exit 1; }

View file

@ -42,7 +42,7 @@ install() {
case "${1:-}" in
fetch)
PB="${PRODUCTION_BRANCH:-production}"
PB="${PRODUCTION_BRANCH:-main}"
cd /app/repo || exit 1
# Its own ref and no FETCH_HEAD, exactly as build-cdn.sh: the dev server's pull loop uses FETCH_HEAD.
if ! git fetch -q --no-write-fetch-head --depth 1 origin "+refs/heads/$PB:refs/cdn/$PB"; then