Cloud container: subdomain apps, a published plugin as its own public site
SUBDOMAIN_APPS=host=bundle/profile makes e.g. research-gallery.<domain> serve the /cdn build of doxa-maps-spa at its root. A separate server on its own port answers only those hosts and only with published /cdn files (no admin, PocketBase or dev server behind it), so no sign-in is skipped anywhere else. The root page gets a <base href> at the plugin's real folder, so its relative links resolve as on /cdn.
This commit is contained in:
parent
12d7f218f3
commit
ded58aa2cc
2 changed files with 36 additions and 0 deletions
|
|
@ -68,6 +68,7 @@ there. Only then does it swap the new build in.
|
|||
| `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 |
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
// its /api/ is PUBLIC (PocketBase signs people in itself), except _superusers and _system collections
|
||||
// /admin/build POST: build PRODUCTION_BRANCH now (OIDC_ADMIN_GROUPS only); /admin/status GET.
|
||||
// The public build also follows PRODUCTION_BRANCH by itself (CDN_AUTOBUILD_MINUTES, default 2).
|
||||
// SUBDOMAIN_APPS public subdomain sites for published plugins, on a separate port (see below)
|
||||
// /admin/pb-signin-check a page that signs in to the team PocketBase and shows who it thinks you are
|
||||
// Groups come from oauth2-proxy's X-Forwarded-Groups, which it sets on signed-in routes only.
|
||||
import { spawn } from 'node:child_process'
|
||||
|
|
@ -65,6 +66,40 @@ if (AUTO_MIN > 0) {
|
|||
console.log(`[admin] public build follows ${PROD}, ${AUTO_MIN} min after it moves`)
|
||||
}
|
||||
|
||||
// SUBDOMAIN APPS: a published plugin as its own public site, e.g. research-gallery.<domain> → the /cdn build
|
||||
// of doxa-maps-spa. SUBDOMAIN_APPS = "host=bundle/profile,host2=bundle/profile". A SEPARATE server on its own
|
||||
// port, reached by the proxy only for those hosts: it serves the published /cdn files and nothing else (no
|
||||
// admin, no PocketBase, no dev server), so no sign-in is skipped anywhere else. "/" is the plugin's page with a
|
||||
// <base href> at its real folder, so its relative links resolve exactly as they do on /cdn.
|
||||
const SUBDOMAINS = Object.fromEntries((env.SUBDOMAIN_APPS || '').split(',').map((x) => x.trim()).filter(Boolean)
|
||||
.map((x) => x.split('=').map((y) => y.trim())).filter(([h, t]) => h && /^[a-z0-9-]+\/[a-z0-9-]+$/.test(t || '')))
|
||||
if (Object.keys(SUBDOMAINS).length) {
|
||||
Bun.serve({
|
||||
hostname: '0.0.0.0',
|
||||
port: Number(env.SUBDOMAIN_PORT || 5176),
|
||||
idleTimeout: 0,
|
||||
async fetch(req) {
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') return text('read-only', 405)
|
||||
const target = SUBDOMAINS[(req.headers.get('host') || '').split(':')[0].toLowerCase()]
|
||||
if (!target) return text('not found', 404)
|
||||
const url = new URL(req.url)
|
||||
const headers = { 'access-control-allow-origin': '*', 'cache-control': 'public, max-age=300, stale-while-revalidate=86400' }
|
||||
if (url.pathname === '/' || url.pathname === '/index.html') {
|
||||
const leaf = join(CDN, 'dx-maps', target, 'index.html')
|
||||
if (!existsSync(leaf)) return text('this app is not published yet', 503)
|
||||
const html = readFileSync(leaf, 'utf8').replace(/<head([^>]*)>/i, `<head$1><base href="/dx-maps/${target}/">`)
|
||||
return new Response(html, { headers: { ...headers, 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' } })
|
||||
}
|
||||
const f = normalize(join(CDN, decodeURIComponent(url.pathname)))
|
||||
if (!f.startsWith(CDN + '/dx-maps/')) return text('not found', 404)
|
||||
const file = existsSync(f) && statSync(f).isDirectory() ? join(f, 'index.html') : f
|
||||
if (!existsSync(file)) return text('not found', 404)
|
||||
return new Response(Bun.file(file), { headers })
|
||||
},
|
||||
})
|
||||
console.log(`[admin] subdomain apps: ${Object.entries(SUBDOMAINS).map(([h, t]) => `${h} → ${t}`).join(', ')}`)
|
||||
}
|
||||
|
||||
Bun.serve({
|
||||
hostname: '127.0.0.1',
|
||||
port: 5175,
|
||||
|
|
|
|||
Loading…
Reference in a new issue