Cloud container: DEV_SERVER=off runs a tenant without the Plugin Studio dev server

No Vite, no pull loop and no Rudder: /cdn, PocketBase, the admin router and the sign-in stay, and / redirects to
/cdn/. The default stays on, so running containers are unchanged. Off without OIDC_ISSUER refuses to start,
because /pb and /admin would then have nothing in front of them.

Replayed onto development after the public Studio mode landed. Off together with ACCESS=public is refused
too, because the public Studio is the dev server itself. PULL_INTERVAL=0 still means no pulls, whatever
DEV_SERVER says. The setting is in the cloud README's table.
This commit is contained in:
Doxa Maps 2026-09-28 18:44:39 +00:00
parent 3268105643
commit e34fac45d4
3 changed files with 32 additions and 5 deletions

View file

@ -25,6 +25,7 @@ and the hot-reload websocket. Only people in the allowed groups get in.
| `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 |
| `DEV_SERVER` | no (`on`) | `off` runs a tenant without the Plugin Studio dev server: no Vite, no pulls, no Rudder; `/` redirects to `/cdn/`. Needs the sign-in (`OIDC_ISSUER`); refused with `ACCESS=public` |
| `HMR_CLIENT_PORT`, `HMR_PROTOCOL` | behind HTTPS | `443` and `wss`, so browsers reach the hot-reload websocket |
## Where it runs

View file

@ -165,6 +165,8 @@ Bun.serve({
async fetch(req) {
const url = new URL(req.url)
const p = url.pathname
// DEV_SERVER=off: there is no Plugin Studio at /, so send people to the public build.
if (p === '/' && env.DEV_SERVER === 'off') return Response.redirect(new URL('/cdn/', url), 302)
if (p.startsWith('/cdn/')) {
const f = normalize(join(CDN, decodeURIComponent(p.slice(5))))
if (!f.startsWith(CDN)) return text('bad path', 400)

View file

@ -3,16 +3,28 @@
# 1. install, 2. pull BRANCH every PULL_INTERVAL s (Vite hot-reloads what changed; 0 = never pull),
# 3. serve: Vite alone; behind an OIDC sign-in when OIDC_ISSUER is set; or, with ACCESS=public, the Studio
# open to everyone and the admin parts behind ADMIN_USER/ADMIN_PASSWORD.
# DEV_SERVER=off (default on) runs a tenant WITHOUT the Plugin Studio dev server: no Vite, no pull loop, no
# Rudder. It keeps /cdn, PocketBase, the admin router and the sign-in, and / redirects to /cdn/. Vite is
# most of the container's memory, so an app-user tenant is a fraction of a building one.
set -u
cd /app/repo
BRANCH="${BRANCH:-main}"; PORT="${PORT:-5173}"
BRANCH="${BRANCH:-main}"; PORT="${PORT:-5173}"; DEV_SERVER="${DEV_SERVER:-on}"
case "$DEV_SERVER" in on|off) ;; *) echo "refusing to start: DEV_SERVER must be on or off (got '$DEV_SERVER')" >&2; exit 1;; esac
if [ "$DEV_SERVER" = off ] && [ "${ACCESS:-}" = public ]; then
echo "refusing to start: DEV_SERVER=off and ACCESS=public don't combine (the public Studio IS the dev server)" >&2
exit 1
fi
echo "[dev-server] $DEV_SERVER"
echo "$(date -u +%FT%TZ) START at $(git rev-parse --short HEAD)" | tee -a /tmp/pull.log
bun install --frozen-lockfile || bun install
# PULL_INTERVAL=0: never pull, never reset. For a checkout mounted from someone's machine, where the
# loop's `git reset --hard` would wipe their work.
# loop's `git reset --hard` would wipe their work. The loop only feeds Vite, so DEV_SERVER=off has none.
# An if-block (not `[ ] && ( ) &`), so no background job ends at once and trips the `wait -n` at the bottom.
if [ "${PULL_INTERVAL:-15}" = 0 ]; then
echo "$(date -u +%FT%TZ) PULL_INTERVAL=0: not following $BRANCH; the checkout is left as it is" | tee -a /tmp/pull.log
elif [ "$DEV_SERVER" = off ]; then
echo "$(date -u +%FT%TZ) DEV_SERVER=off: no dev server, so no pull loop" | tee -a /tmp/pull.log
else
(
while true; do
@ -66,7 +78,9 @@ fi
# in the BACKGROUND so the site serves at once: until it finishes, /api/rudder answers "starting", then
# the cloud Vite config restarts the dev server once, which starts the engine. /api/rudder stays behind
# the sign-in (it is not a public route). About 1 GB resident.
if [ "${RUDDER_ENGINE:-0}" = 1 ]; then
if [ "${RUDDER_ENGINE:-0}" = 1 ] && [ "$DEV_SERVER" = off ]; then
echo "[rudder] skipped: it runs inside the dev server, and DEV_SERVER=off"
elif [ "${RUDDER_ENGINE:-0}" = 1 ]; then
if [ -z "${RUDDER_HOME:-}" ]; then
if ls /data/rudder/models/*.gguf >/dev/null 2>&1; then RUDDER_HOME=/data/rudder; else RUDDER_HOME=/opt/rudder; fi
fi
@ -92,6 +106,11 @@ if [ "${RUDDER_ENGINE:-0}" = 1 ]; then
disown
fi
if [ -z "${OIDC_ISSUER:-}" ] && [ "$DEV_SERVER" = off ]; then
# Without oauth2-proxy nothing stands in front of /pb and /admin (their admin check trusts its headers).
echo "refusing to start: DEV_SERVER=off serves /pb and /admin, which need the sign-in; set OIDC_ISSUER" >&2
exit 1
fi
if [ -z "${OIDC_ISSUER:-}" ] && [ "${ACCESS:-}" != public ]; then
# Open, like a laptop: no sign-in, no backend.
export VITE_HOST=0.0.0.0 VITE_PORT="$PORT"
@ -176,7 +195,12 @@ if [ -n "${PB_OIDC_CLIENT_SECRET:-}" ]; then
fi
fi
VITE_HOST=127.0.0.1 VITE_PORT=5174 bun x vite --config "$CONFIG" &
if [ "$DEV_SERVER" = on ]; then
VITE_HOST=127.0.0.1 VITE_PORT=5174 bun x vite --config "$CONFIG" &
ROOT=http://127.0.0.1:5174/
else
ROOT=http://127.0.0.1:5175/ # the admin router: / redirects to /cdn/
fi
oauth2-proxy \
--provider=oidc \
--oidc-issuer-url="$OIDC_ISSUER" \
@ -188,7 +212,7 @@ oauth2-proxy \
"${ALLOW[@]}" \
--email-domain='*' \
--provider-display-name="${OIDC_DISPLAY_NAME:-your identity provider}" \
--upstream=http://127.0.0.1:5174/ \
--upstream="$ROOT" \
--upstream=http://127.0.0.1:5175/cdn/ \
--upstream=http://127.0.0.1:5175/api-prod/ \
--upstream=http://127.0.0.1:5175/pb/ \