Single portfolio hub for public web and AI projects from github.com/sachinsshetty, github.com/slabstech, and github.com/dwani-ai. Version 1 lists the union of qualifying web apps and AI applications (not “must be both”). Infra-only, native-only, and library-only repositories are omitted from the catalog.
The UI loads web/public/projects.v1.json. Each card has Run in UberApp (embedded preview at /run/<project-id>), New tab, and Document. Each row includes a subdomain field equal to id: use that string as the Docker Compose service name and as the left-hand label in Host(<subdomain>.${DOMAIN}) for Traefik.
Everything is wired in one project: Node scripts under scripts/, a root package.json, and a Makefile.
| Command | What it does |
|---|---|
npm run env:copy |
Create .env from .env.example if missing |
npm run install:web |
npm ci in web/ |
npm run manifest |
Regenerate web/public/projects.v1.json (reads DOMAIN from .env) |
npm run validate |
docker compose config (includes docker-compose.apps.yml if present) |
npm run compose:apps |
Regenerate docker-compose.apps.yml (simple-deploy-repos.mjs + runtime-node-repos.mjs + runtime-python-repos.mjs) |
npm run stubs |
Regenerate traefik/dynamic/projects-stubs.yml (needs DOMAIN; see below) |
npm run deploy |
Full pipeline: install:web → manifest → compose:apps → stubs → compose validate → build → docker compose up -d |
npm run deploy:build |
Same as deploy but no up (images only) |
npm run ci |
Same checks as GitHub Actions (needs Docker on the machine) |
Equivalent: make deploy, make manifest, make ci, etc.
Simple deploy vs runtime vs coming soon: Manifest liveUrl and generated docker-compose.apps.yml cover static repos from scripts/lib/simple-deploy-repos.mjs (image runtime/catalog-app: build.sh runs MkDocs when mkdocs.yml is present, Jekyll when _config.yml + _layouts/ exist, otherwise finds a package.json with build — including talk-ui/ — then nginx), Node apps from scripts/lib/runtime-node-repos.mjs (e.g. Escape Among Us → runtime/escape-among-us, port 3000), and Python apps from scripts/lib/runtime-python-repos.mjs (e.g. Workshop Gradio → runtime/workshop, port 8000 with DWANI_API_KEY / DWANI_API_BASE_URL; Agent Olympics School → runtime/agent-olympics-school: Gradio on 8080 (Traefik) plus a sidecar FastAPI on 8000 on the Docker network only, with TUTOR_API_KEY and DWANI_API_BASE_URL for Knowunity + Dwani). Remaining catalog rows stay liveUrl: null (Coming soon) until you add a service. Web-only rows without a deploy path get a short description in web/public/projects.v1.json (from web/scripts/gen-manifest.mjs) explaining why (placeholder repo, Android-only, etc.).
First-time server deploy:
cp .env.example .env
# edit DOMAIN=portfolio.example.com
npm run deployNon-interactive / automation: if .env is missing, you can bootstrap in one step (no manual edit):
DOMAIN=portfolio.example.com node scripts/deploy.mjsGCP Compute Engine (single bootstrap):
- Allow TCP 80 to the VM (e.g. create a VPC firewall rule, or use the
http-servernetwork tag with the default rule). - Point DNS:
hub.<DOMAIN>and*.<DOMAIN>(wildcard) at the VM’s external IP. - Create the instance with metadata and the startup script (run from a machine that has this repo, or adjust paths):
gcloud compute instances create uber-app \
--zone=us-central1-a \
--machine-type=e2-standard-4 \
--boot-disk-size=100GB \
--tags=http-server \
--metadata-from-file=startup-script=scripts/gcp-startup.sh \
--metadata=uberapp_domain=portfolio.example.comOptional metadata key uberapp_repo_url if the install should clone a fork. The script installs Docker and Node 22, clones into /opt/uber-app, and runs node scripts/deploy.mjs with DOMAIN set. The first run may take a long time if every catalog app image is built. If users always open the site as HTTPS (e.g. Cloudflare), add UBERAPP_URL_SCHEME=https to /opt/uber-app/.env, then run npm run manifest && node scripts/deploy.mjs --skip-install so iframe liveUrls match.
Existing VM (Docker + Node 20+ already installed):
git clone https://github.com/dwani-ai/uber-app.git && cd uber-app
DOMAIN=your.apex.domain node scripts/deploy.mjsOptional flags for scripts/deploy.mjs: --skip-install, --skip-manifest, --no-up.
If you add docker-compose.apps.yml (copy from docker-compose.apps.example.yml), it is merged automatically by all compose-related commands.
On the VM, Traefik is the only public entry (port 80 by default). UberApp (hub) and every project run as separate containers on the shared Docker network uberapp_edge.
| Host | Container |
|---|---|
hub.${DOMAIN} |
This repo’s hub image (static UI) |
<project-id>.${DOMAIN} |
Default: stub page (placeholder service). Optional: your real container via docker-compose.apps.yml (same service name as catalog id) |
All catalog projects “running”
After npm run deploy (or npm run stubs), Traefik loads projects-stubs.yml: one Host(<id>.${DOMAIN}) rule per catalog entry, all pointing at a single nginx placeholder container that returns a small HTML page (so every subdomain answers — no 502). To run the real app for an id, add it to docker-compose.apps.yml with Traefik labels; that service name is auto-omitted from stub generation so routes do not conflict.
Set UBERAPP_URL_SCHEME / UBERAPP_PUBLIC_PORT in .env when Traefik is not on 443 (local example: http + 8080). Regenerate the manifest after changing them.
Stub file is gitignored when generated locally; traefik/dynamic/00-bootstrap.yml keeps the directory valid before the first npm run stubs.
DNS (Cloudflare or any DNS):
Ahub → VM IP (or CNAME).A* (wildcard) → same VM IP so every<id>.${DOMAIN}resolves without adding 50+ records (or create individualArecords if you prefer).
Copy .env.example to .env and set DOMAIN, or run npm run env:copy once.
npm run deploy(Equivalent: docker compose build && docker compose up -d after npm run manifest, but deploy runs the full sequence.)
Traefik listens on 80 on the host. If port 80 is busy locally, set e.g. TRAEFIK_HTTP_PORT=8080 in .env and open http://hub.${DOMAIN}:8080 only if your DNS/hosts match (usually use plain :80 on the server).
Each catalog id (e.g. dwani-ai-discovery) must match:
- Docker Compose
services.<id>name (letters, digits,-; same as manifestid). - Traefik rule
Host(\.${DOMAIN}`)` pointing at that service’s internal port.
See docker-compose.apps.example.yml for a working whoami demo and a commented template. Merge when starting:
docker compose -f docker-compose.yml -f docker-compose.apps.yml up -d --buildBuild or pull images from each project’s own repository; UberApp does not vendor those Dockerfiles.
When DOMAIN or UBERAPP_DEPLOY_DOMAIN is set, regenerating the manifest assigns every project:
https://<id>.<DOMAIN>
so Run in UberApp / iframes target your Traefik routes (after those containers exist).
npm run manifest
# or: cd web && DOMAIN=portfolio.example.com npm run gen:manifestWithout DOMAIN, the generator keeps GitHub Pages defaults for web rows and liveUrl: null for AI-only rows (unless you edit JSON by hand).
subdomain is always written and matches id for Traefik/DNS alignment.
To run the same stack as production on your machine (Traefik + hub, optional app services):
-
Pick a hostname that resolves to your laptop without editing
/etc/hostsfor every app: use nip.io with your loopback address, for example127.0.0.1.nip.io. Thenhub.127.0.0.1.nip.io,dwani-ai-discovery.127.0.0.1.nip.io, etc. all resolve to127.0.0.1. -
Create
.env(or runnpm run env:copyand edit):DOMAIN=127.0.0.1.nip.io TRAEFIK_HTTP_PORT=8080
Use
8080(or another free port) if something already uses 80. -
From the repo root (install Node + Docker):
npm run manifest # writes liveUrl = https://<id>.127.0.0.1.nip.io (needs rebuild to bake into hub image) npm run deployThe hub image is built after the manifest is generated, so the static UI includes the updated JSON.
-
Open http://hub.127.0.0.1.nip.io:8080 (include the port if
TRAEFIK_HTTP_PORT=8080).
Port mismatch: npm run manifest with DOMAIN set produces liveUrl values like https://<id>.127.0.0.1.nip.io (no port). If Traefik is only on 8080, those links target 443 and will fail until you either publish Traefik on 80 locally, or change liveUrl manually to http://<id>.127.0.0.1.nip.io:8080 for dev, or put a proxy on 80.
“All web apps running” in Docker
- UberApp does not include built images for every catalog repo. Traefik can only route to containers you define.
- For each app you actually have an image for, add a service to
docker-compose.apps.yml(start fromdocker-compose.apps.example.yml): service name = manifestid, TraefikHost(\.${DOMAIN}`)` → container port. - Apps without a compose service will get no backend (Traefik 502) if the manifest’s
liveUrlpoints at that subdomain. To preview many sites without running 58 containers, keep the committed manifest without settingDOMAINwhen you runnpm run manifestso web apps keep GitHub PagesliveUrls and embed those instead.
Hub only (no Traefik)
docker compose build hub
docker run --rm -p 8080:80 uber-app-hub:localOpen http://localhost:8080. Use a manifest with liveUrls that already work (e.g. GitHub Pages).
Requirements: Node.js 20+ (or Docker only).
cd web
npm ci
npm run devOpen the URL Vite prints (default http://localhost:5173).
- Edit
web/public/projects.v1.json, or - Change lists in
web/scripts/gen-manifest.mjsand runnpm run gen:manifestfromweb/.
Per-repo overrides live in liveUrlOverrides when not using DOMAIN-based URLs.
The manifest may include "$schema": "/projects.v1.schema.json". See web/public/projects.v1.schema.json.
.github/workflows/ci.yml runs node scripts/ci.mjs: web install + production build, generate-traefik-stubs, docker compose config, and docker compose build (hub + placeholder).
- Create a VM with Docker; allow 80 (and 443 if you terminate TLS on the VM later).
- Clone this repo, configure
.envwithDOMAIN. - Run
npm run deploy. Add project services by creatingdocker-compose.apps.yml(see example) and redeploying. - In Cloudflare, point hub and a wildcard
*(or individual hosts) to the VM IP. Choose an SSL mode that matches whether Traefik serves HTTP only (Flexible to origin) or you add TLS on Traefik.
| Path | Purpose |
|---|---|
package.json (root) |
npm scripts: deploy, manifest, ci, … |
Makefile |
Same as npm scripts for make users |
scripts/deploy.mjs |
Automated install → manifest → Traefik stubs → compose build → up |
scripts/generate-traefik-stubs.mjs |
One Host() route per catalog id → placeholder |
traefik/dynamic/00-bootstrap.yml |
Minimal file before projects-stubs.yml exists |
traefik/dynamic/projects-stubs.yml |
Generated (gitignored): all stub routers |
placeholder/ |
Shared nginx for stub subdomains |
scripts/ci.mjs |
CI pipeline entrypoint |
scripts/lib/ |
Shared .env loader + compose file list |
web/ |
Vite + React + TypeScript hub |
web/src/pages/ |
Home catalog + /run/:projectId embed |
web/public/projects.v1.json |
v1 catalog (subdomain + liveUrl) |
web/scripts/gen-manifest.mjs |
Regenerates catalog; respects DOMAIN |
docker-compose.yml |
Traefik + hub + placeholder |
docker-compose.apps.yml |
Optional — your project services (gitignored if local) |
docker-compose.apps.example.yml |
Example merge file |
.env.example |
DOMAIN and optional port |
Dockerfile |
Hub image (nginx static) |
nginx.conf |
SPA fallback inside hub |