Cloud dev server recipe in the repo, with an optional OIDC sign-in
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

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.
This commit is contained in:
Doxa Maps 2026-09-27 07:38:08 +00:00
parent c42fc4471b
commit f57435253c
5 changed files with 161 additions and 0 deletions

View file

@ -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=<name>` 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

View file

@ -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"]

View file

@ -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://<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.

65
deployment/cloud/entry.sh Executable file
View file

@ -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 $?

View file

@ -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 } : {}),
},
})
}