From f57435253ca607655848a723c02e5125a35280d6 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sun, 27 Sep 2026 07:38:08 +0000 Subject: [PATCH] Cloud dev server recipe in the repo, with an optional OIDC sign-in deployment/cloud/: a bootstrap Dockerfile (Bun, git, oauth2-proxy v7.15.3) that clones the repo and runs deployment/cloud/entry.sh from it, so the recipe updates with git; the Vite overlay (host, allowed hosts, HMR over wss); and the pull loop that follows the branch. With no OIDC env it serves Vite directly (laptop default unchanged). With OIDC_ISSUER set, oauth2-proxy owns the port and Vite listens on localhost behind it: Studio, maps and the HMR websocket all sit behind one sign-in, with PKCE S256, a confidential client and a REQUIRED group allow-list (it refuses to start without one). Secrets reach oauth2-proxy through its environment only. README gets a short 'Run it in the cloud' section linking the recipe. --- README.md | 8 ++++ deployment/cloud/Dockerfile | 21 +++++++++ deployment/cloud/README.md | 45 ++++++++++++++++++ deployment/cloud/entry.sh | 65 ++++++++++++++++++++++++++ deployment/cloud/vite.cloud.config.mjs | 22 +++++++++ 5 files changed, 161 insertions(+) create mode 100644 deployment/cloud/Dockerfile create mode 100644 deployment/cloud/README.md create mode 100755 deployment/cloud/entry.sh create mode 100644 deployment/cloud/vite.cloud.config.mjs diff --git a/README.md b/README.md index 4b491bf3a..5b51966ec 100644 --- a/README.md +++ b/README.md @@ -616,6 +616,14 @@ The studio is a plugin like any other: `plugin-designs/plugin-studio/plugin-stud | `bun run gates:pre` ยท `bun run gates:post` | The checks that keep `main` working | | `bun run shots` | Retake these screenshots (`ONLY=` for one) | +### Run it in the cloud + +**One shared dev server for the whole team, no laptop setup.** A container clones the repo, follows +`main`, and hot-reloads every change an agent lands, so everyone sees the Plugin Studio update in +their browser within seconds, with no rebuild. Add OIDC settings and it puts a "Sign in with *your +provider*" page in front of everything, open only to the groups you allow. Recipe and settings: +[deployment/cloud/](./deployment/cloud/README.md). + --- ## Repository layout diff --git a/deployment/cloud/Dockerfile b/deployment/cloud/Dockerfile new file mode 100644 index 000000000..b0cf88d11 --- /dev/null +++ b/deployment/cloud/Dockerfile @@ -0,0 +1,21 @@ +# dx-maps cloud dev server: one shared Vite dev server with hot reload for a whole team. +# This image is only a bootstrap: at start it clones the repo and runs deployment/cloud/entry.sh +# FROM THE REPO, so the recipe lives in git and updates with it. Paste this file as-is into a +# container host (Coolify "Dockerfile" build pack, Railway, or `docker build`). +FROM quay.io/oauth2-proxy/oauth2-proxy:v7.15.3 AS oauth2proxy +FROM oven/bun:1-debian +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* +COPY --from=oauth2proxy /bin/oauth2-proxy /usr/local/bin/oauth2-proxy +RUN printf '%s\n' \ + '#!/bin/bash' \ + 'set -u; export GIT_TERMINAL_PROMPT=0' \ + 'T="${GIT_TOKEN:-${FORGEJO_TOKEN:-}}"' \ + 'if [ -n "$T" ]; then printf "#!/bin/sh\ncase \"\$1\" in *sername*) echo \"\$GIT_TOKEN_EFF\";; *) echo;; esac\n" > /tmp/askpass.sh; chmod 700 /tmp/askpass.sh; export GIT_ASKPASS=/tmp/askpass.sh GIT_TOKEN_EFF="$T"; fi' \ + ': "${REPO_URL:?set REPO_URL to the dx-maps git URL}"' \ + '[ -d /app/repo/.git ] || git clone -q --depth 1 --branch "${BRANCH:-main}" "$REPO_URL" /app/repo || { echo "clone failed"; sleep 60; exit 1; }' \ + 'cp /app/repo/deployment/cloud/entry.sh /tmp/entry.sh && exec bash /tmp/entry.sh' \ + > /boot.sh && chmod 755 /boot.sh +ENV PORT=5173 BRANCH=main PULL_INTERVAL=15 +EXPOSE 5173 +CMD ["/boot.sh"] diff --git a/deployment/cloud/README.md b/deployment/cloud/README.md new file mode 100644 index 000000000..c613430cc --- /dev/null +++ b/deployment/cloud/README.md @@ -0,0 +1,45 @@ +# 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 + +1. Build the image from [`Dockerfile`](./Dockerfile). It's a small bootstrap (Bun, git, and the + [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) binary); at start it clones the repo + and runs [`entry.sh`](./entry.sh) from it, so the recipe updates with the repo. Paste it into any + container host (Coolify's "Dockerfile" build pack, Railway, `docker build`). +2. Set the environment below and expose the container's `PORT` over HTTPS. +3. 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 | +| `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 | + +## 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:///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:///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. diff --git a/deployment/cloud/entry.sh b/deployment/cloud/entry.sh new file mode 100755 index 000000000..26c3193d4 --- /dev/null +++ b/deployment/cloud/entry.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# dx-maps cloud dev server, run by the image's /boot.sh from the cloned repo (see Dockerfile). +# 1. install, 2. pull BRANCH every PULL_INTERVAL s (Vite hot-reloads what changed), +# 3. serve: Vite alone, or behind an OIDC sign-in when OIDC_ISSUER is set. +set -u +cd /app/repo +BRANCH="${BRANCH:-main}"; PORT="${PORT:-5173}" +echo "$(date -u +%FT%TZ) START at $(git rev-parse --short HEAD)" | tee -a /tmp/pull.log +bun install --frozen-lockfile || bun install + +( + while true; do + sleep "${PULL_INTERVAL:-15}" + OLD=$(git rev-parse HEAD) + if git fetch -q --depth 1 origin "$BRANCH"; then + NEW=$(git rev-parse FETCH_HEAD) + if [ "$OLD" != "$NEW" ]; then + git reset -q --hard FETCH_HEAD + echo "$(date -u +%FT%TZ) PULLED ${OLD:0:8} -> ${NEW:0:8}" >> /tmp/pull.log + fi + fi + done +) & + +CONFIG=/app/repo/deployment/cloud/vite.cloud.config.mjs + +if [ -z "${OIDC_ISSUER:-}" ]; then + # Open, like a laptop: no sign-in. + export VITE_HOST=0.0.0.0 VITE_PORT="$PORT" + exec bun x vite --config "$CONFIG" +fi + +# OIDC gate: oauth2-proxy owns the public port; Vite listens on localhost behind it, so the Plugin +# Studio, every map and the hot-reload websocket are all behind the same sign-in. +: "${OIDC_CLIENT_ID:?}" "${OIDC_CLIENT_SECRET:?}" "${OIDC_REDIRECT_URL:?}" +if [ -z "${OIDC_ALLOWED_GROUPS:-}" ]; then + echo "refusing to start: set OIDC_ALLOWED_GROUPS (comma-separated). With open sign-up, anyone could log in." >&2 + exit 1 +fi +ALLOW=(); IFS=',' read -ra G <<< "$OIDC_ALLOWED_GROUPS" +for g in "${G[@]}"; do g="${g// /}"; [ -n "$g" ] && ALLOW+=(--allowed-group="$g"); done + +# Secrets reach oauth2-proxy through its environment, never its command line. +export OAUTH2_PROXY_CLIENT_SECRET="$OIDC_CLIENT_SECRET" +export OAUTH2_PROXY_COOKIE_SECRET="${OIDC_COOKIE_SECRET:-$(head -c 32 /dev/urandom | base64 | tr -d '=+/' | head -c 32)}" + +VITE_HOST=127.0.0.1 VITE_PORT=5174 bun x vite --config "$CONFIG" & +oauth2-proxy \ + --provider=oidc \ + --oidc-issuer-url="$OIDC_ISSUER" \ + --client-id="$OIDC_CLIENT_ID" \ + --redirect-url="$OIDC_REDIRECT_URL" \ + --code-challenge-method=S256 \ + --scope="${OIDC_SCOPE:-openid profile email groups}" \ + --oidc-groups-claim="${OIDC_GROUPS_CLAIM:-groups}" \ + "${ALLOW[@]}" \ + --email-domain='*' \ + --provider-display-name="${OIDC_DISPLAY_NAME:-your identity provider}" \ + --upstream=http://127.0.0.1:5174/ \ + --http-address="0.0.0.0:$PORT" \ + --reverse-proxy=true \ + --cookie-secure=true \ + --cookie-samesite=lax & +wait -n +exit $? diff --git a/deployment/cloud/vite.cloud.config.mjs b/deployment/cloud/vite.cloud.config.mjs new file mode 100644 index 000000000..d43d9c7d2 --- /dev/null +++ b/deployment/cloud/vite.cloud.config.mjs @@ -0,0 +1,22 @@ +// Overlay for the cloud dev server: the repo's own vite.config.js plus what a shared, public +// host needs. Every value comes from the environment (see deployment/cloud/README.md). +import base from '/app/repo/vite.config.js' +import { mergeConfig } from 'vite' + +const env = process.env +export default async (e) => { + const b = typeof base === 'function' ? await base(e) : base + const hmr = env.HMR_CLIENT_PORT + ? { clientPort: Number(env.HMR_CLIENT_PORT), protocol: env.HMR_PROTOCOL || 'wss' } + : undefined + return mergeConfig(b, { + cacheDir: '/tmp/vite-cache', + server: { + host: env.VITE_HOST || '0.0.0.0', + port: Number(env.VITE_PORT || 5173), + strictPort: true, + allowedHosts: true, + ...(hmr ? { hmr } : {}), + }, + }) +}