A Nebari Software Pack for launching, managing, and observing web
apps on a Nebari Kubernetes cluster, behind Keycloak SSO. Two kinds of app are
supported: static sites (HTML/CSS/JS, served by nginx) and Python apps launched
by a pixi task — upload a zip with a pixi.toml and your source, name
the task, and the pack runs it. No Dockerfile, no image build.
Apps can be launched four ways, all converging on a single declarative App resource:
- 🖥️ A form-based UI — dashboard with analytics, app detail with logs/events, a launch form, and direct zip/.html upload.
- 🌐 A REST API for programmatic CRUD + observability.
- 🤖 A coding agent (Claude Code, Codex) — connect to the in-cluster MCP server at
https://apps.<cluster-domain>/mcpand launch with natural language. - 🔧 The MCP server directly (launch / list / status / logs / update / stop / remove as agent tools; Keycloak device-flow auth).
Status: Alpha (
level: alphainpack-metadata.yaml) — installs and runs the happy path on a current Nebari dev cluster; APIs, chart values and theAppCRD may still change between releases. Read Known limitations before demoing. Implemented: theAppCRD and apps-operator (static + Python/pixi apps frominline/git/pvcsources), the apps-api (CRUD, logs/events/status, analytics, zip/.html upload), the apps-ui (Nebari design system: dashboard + analytics, app detail with logs, launch form with upload), the apps-mcp server (agent tools + device-flow auth at/mcp), and thenew-nebari-appskill (scaffold apps with anebari-app.yamlmanifest, then "launch it"). Apps are served athttps://<subdomain>.apps.<cluster-domain>(TLS can be switched off viatls.enabled=false, as local dev does).
Every producer — agent/MCP, API, UI, or (optional) GitOps — writes the same App custom
resource (apps.nebari.dev/v1alpha1). The apps-operator reconciles it into a Deployment +
Service + a NebariApp; the existing nebari-operator turns that
NebariApp into routing (HTTPRoute), TLS (cert-manager), Keycloak SSO, and a landing-page
tile.
UI ────────┐
REST API ──┤ ┌─ Deployment (app pod)
agent/MCP ─┼─► App CR ─► apps- ─┼─ Service
GitOps ────┘ (one operator └─ NebariApp ─► nebari-operator ─► HTTPRoute + TLS
contract) + Keycloak + tile
The apps-api is the single authority — the UI and MCP are thin clients over it, so launch
semantics are identical whether a human uses the form or an agent uses natural language. The API
writes App CRs directly via a ServiceAccount, so GitHub/GitOps is optional, not required.
| Component | Stack | Responsibility |
|---|---|---|
| App CRD | apps.nebari.dev/v1alpha1 |
The declarative contract for an app. |
| apps-operator | Go + controller-runtime | Reconcile App → Deployment, Service, NebariApp, status. |
| apps-api | Python / FastAPI | CRUD + observability + zip/.html upload; writes App CRs. |
| apps-ui | React + TS + Vite + nebari-design | Dashboard + analytics, launch form, app detail with logs/events. |
| apps-mcp | Python / FastMCP | Agent-facing tools at /mcp; Keycloak device-flow auth. |
| skill | Claude Code skill | Scaffold static apps + nebari-app.yaml; "launch it" via the MCP. |
- apps-ui / apps-api — the UI logs in with keycloak-js (SPA PKCE flow against a public
client its
NebariAppprovisions) and calls the API with a JWT bearer token, which the API validates against the realm JWKS. - Launched apps — no auth code at all: private apps are enforced at the gateway by the
SecurityPolicytheirNebariAppcreates;access.public: trueskips it.
The UI can be rebranded without rebuilding the image — title, logos, favicon, theme
colors, classification banners, and its landing-page tile. Set ui.title / ui.branding.*
in the chart (rendered into a /config.json ConfigMap the UI fetches at startup), or
BRANDING_* env vars when running the image standalone. This is the same contract used by
nebari-landing, llm-serving-pack, and provenance-collector-pack, so a fleet rebrands
uniformly. See the Branding docs.
apps-pack/
pack-metadata.yaml # dashboard registration
charts/nebari-apps/ # Helm chart (App CRD + operator + api + ui + mcp)
operator/ # Go operator (controller-runtime)
api/ # FastAPI backend (CRUD + observability + upload)
ui/ # React frontend (Nebari design system)
mcp/ # FastMCP server (agent tools at /mcp)
skill/ # new-nebari-app scaffolding skill (Claude Code)
dev/ # local dev loop (kind + full Nebari stack)
examples/ # sample App CRs
docs/ # user guide (Astro Starlight → packs.nebari.dev)
src/content/docs/ # guide pages (getting started, MCP, skill, references)
DESIGN.md # comprehensive design document
PLAN.md # phased implementation plan
Zip your site (or take a single .html file) and upload it from the UI's launch form, or via
the API:
curl -X POST https://apps.<cluster-domain>/api/v1/apps/upload \
-H "Authorization: Bearer $TOKEN" \
-F 'manifest={"name":"docs-site","namespace":"apps","displayName":"Docs Site",
"access":{"public":true,"subdomain":"docs-site"}}' \
-F "file=@site.zip"The uploaded files become the
App's inline source, materialized on the cluster as a ConfigMap-backed volume (text assets, ~900KB cap — use agitorpvcsource for larger sites). An archive needs anindex.htmlat its root.
Or write the App CR yourself — static apps can be sourced from inline files, a git
repository (cloned by an init container), or an existing pvc:
apiVersion: apps.nebari.dev/v1alpha1
kind: App
metadata:
name: team-site
spec:
displayName: "Team Site"
source:
type: git
git: { url: "https://github.com/org/site", ref: "main", subdir: "public" }
access:
public: false
groups: ["analysts"]
subdomain: team-sitekubectl apply it (or POST /api/v1/apps, or use the UI's launch form) → reachable at
https://team-site.apps.<cluster-domain> behind Keycloak SSO.
Install the scaffolding skill and connect the MCP server:
cp -r skill/new-nebari-app .claude/skills/ # or ~/.claude/skills/
claude mcp add --transport http nebari-apps https://apps.<cluster-domain>/mcpThen, in Claude Code:
You: create a public static site called release-notes (the skill scaffolds
release-notes/with real files +nebari-app.yaml) You: launch it (the agent reads the manifest, callslaunch_app, polls status, and replies withhttps://release-notes.apps.<cluster-domain>)
The manifest maps 1:1 onto App.spec, so the same directory works with the UI's upload
form, the API, or GitOps.
- A Nebari cluster running the Nebari Infrastructure Core stack: nebari-operator
v0.1.0-alpha.20or later (provides theNebariAppCRD; this is the version the pack's contract test and local dev loop pin), Envoy Gateway with the sharedpublic(orinternal) Gateway, a Keycloak realm (nebariby default), and cert-manager with a cluster issuer when TLS is enabled (the default). - Helm 3.8+ and
kubectlwith cluster-admin for the install (the chart creates a CRD and ClusterRoles). - Namespace opt-in: the nebari-operator only reconciles namespaces labeled
nebari.dev/managed=true. Label the release namespace and every namespace inapi.allowedNamespaces(defaultapps). - Sizing: the pack itself requests 300m CPU / 448Mi memory (limits 1.5 CPU / 896Mi)
across its four pods. Each launched app adds a pod with whatever
runtime.resourcesthe App sets (there is no default; see Known limitations) — budget roughly 100m / 128Mi for a static site and 500m / 1Gi for a Python app with a coldpixi install. - Optional: metrics-server (per-app CPU/memory in the UI; the UI degrades gracefully without it) and the LGTM observability pack for logs.
Alpha means the happy path works; these are the gaps to know about before a demo or pilot.
access.groups/access.usersare not enforced at the gateway. The nebari-operator0.1.0SecurityPolicy authenticates but does not authorize by group, so a private app (access.public: false) is reachable by any signed-in realm user;groupsis only forwarded to theNebariApp.access.usersis not used at all, so the API rejects it with422. Treatpublic: falseas "authenticated", not "authorized". Upstream: nebari-operator#153.- Authorization is owner-or-admin only. The API lets a user see and manage only the
Apps they launched; members of
api.adminGroupsmanage all of them. There is no sharing or delegation yet, and anyone withkubectlaccess to an app namespace bypasses the API. - Inline sources are capped at ~900 KB of UTF-8 text (a ConfigMap). Binary assets or
larger sites need a
gitorpvcsource. - No resource defaults or quotas for launched apps. An App that omits
runtime.resourcesruns unbounded;replicashas no upper bound via the API or kubectl (the MCP tool caps it at 10). Apply aLimitRange/ResourceQuotato app namespaces. - Subdomains are not checked for uniqueness across Apps; two Apps claiming the same
access.subdomainboth emit aNebariAppfor one hostname. - One API version (
v1alpha1) and no conversion webhook — a future schema change will require re-creating Apps.helm upgradedoes not update CRDs; re-applycharts/nebari-apps/crds/by hand on upgrades. - The MCP server must run as a single replica: device-flow sessions are held in process memory.
- Landing-page tiles are off by default for launched apps (
apps.landingPage.enabled/spec.landingPage.enabled); only the pack's own "Apps" tile is registered. - Local dev disables authentication (
api.auth.enabled=false, withtls.enabled=false; the chart refuses to render it with TLS on unlessapi.auth.insecureAllowAnonymous=true); never carry that flag to a real cluster.
On a Nebari cluster (nebari-operator installed):
helm install nebari-apps charts/nebari-apps \
--namespace nebari-apps --create-namespace \
--set clusterDomain=<your-cluster-domain> \
--set keycloak.url=https://keycloak.<your-cluster-domain>/auth
kubectl label namespace nebari-apps nebari.dev/managed=true
kubectl create namespace apps && kubectl label namespace apps nebari.dev/managed=trueOr install from the published chart with the ready-made values file
(examples/nebari-values.yaml — only the two hostnames need
editing):
helm install nebari-apps oci://ghcr.io/nebari-dev/apps-pack/charts/nebari-apps \
--namespace nebari-apps --create-namespace -f examples/nebari-values.yamlThe UI lands at https://apps.<your-cluster-domain> (keycloak-js SSO) with the
MCP endpoint at https://apps.<your-cluster-domain>/mcp; every launched app
gets https://<name>.apps.<your-cluster-domain> with auth enforced at the
gateway. Connect a coding agent:
claude mcp add --transport http nebari-apps https://apps.<your-cluster-domain>/mcpApps can also be applied directly:
kubectl label namespace <your-namespace> nebari.dev/managed=true
kubectl apply -n <your-namespace> -f examples/static-inline-app.yaml
kubectl get apps -n <your-namespace> -w # wait for Phase=Running, then open the URLLocally (kind + the full Nebari stack, ~5–10 min first run):
cd dev
make up # cluster + operator + api + ui + mcp + example app (plain HTTP, no auth)
open http://apps.nebari.test # the UI
open http://docs-site.apps.nebari.test # the example appNo port-forwarding or /etc/hosts edits: a local wildcard-DNS container resolves
*.nebari.test to 127.0.0.1 (one-time /etc/resolver file on macOS, offered by make up),
and the kind cluster maps host port 80 straight to the gateway, so every launched app is
reachable as soon as its route reconciles. Type the http:// scheme explicitly: browsers
auto-upgrade bare hostnames to https, which is disabled locally. See the
local development guide for the full dev loop.
Merges to main continuously publish latest + sha-tagged images to GHCR
(build-image.yaml). Versioned releases are cut by
publishing a GitHub release with a vX.Y.Z tag, which triggers
release.yaml to:
- build and push all four images (
apps-operator,apps-api,apps-ui,apps-mcp) to GHCR taggedX.Y.ZandX.Y, and - package the Helm chart with
version/appVersionset toX.Y.Zand push it tooci://ghcr.io/nebari-dev/apps-pack/charts.
Install a released version straight from the OCI registry (the chart's image tags default
to its appVersion, so the matching images come along automatically):
helm install nebari-apps oci://ghcr.io/nebari-dev/apps-pack/charts/nebari-apps \
--version <X.Y.Z> \
--namespace nebari-apps --create-namespace \
--set clusterDomain=<your-cluster-domain> \
--set keycloak.url=https://keycloak.<your-cluster-domain>/authIssue tracking follows along automatically
(project-status.yaml): merging a PR into main
moves its linked issues' project status from In Progress / In Review to Merged,
and publishing a release moves every Merged issue to Released. Updates are made as
the nebari-project-sync GitHub App (via the NEBARI_PROJECT_SYNC_APP_ID /
NEBARI_PROJECT_SYNC_APP_PRIVATE_KEY org secrets), so project history attributes them to
the bot rather than a person; the built-in GITHUB_TOKEN cannot access organization
Projects v2. The release job also requires the project board to be linked to this
repository (repo Projects tab → Link a project).
The user guide lives at packs.nebari.dev/apps-pack
(an Astro Starlight site in docs/; run it locally
with cd docs && bun install && bun run dev):
- Getting started, launching apps (UI / upload / API / kubectl), the MCP server (connecting agents, device-flow auth, tool list), the scaffolding skill, and local development
- Reference: chart configuration (every value and the
NebariAppfields the chart emits), the App CRD, the REST API, and architecture & auth
Design documents stay in the repository:
docs/DESIGN.md— the comprehensive design document.docs/PLAN.md— phased implementation roadmap and risks.- software-pack-template — pack
conventions and the
NebariAppCRD reference this pack builds on.