Getting started
Prerequisites
Section titled “Prerequisites”- A Nebari cluster running the Nebari Infrastructure Core stack:
nebari-operator
v0.2.0or later (provides theNebariAppCRD; the version this pack’s contract test and dev loop pin), Envoy Gateway with the sharedpublic(orinternal) Gateway, and a Keycloak realm (nebariby default). - cert-manager with a cluster issuer — only 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 apps may be launched into. - Sizing: the pack requests 300m CPU / 448Mi memory (limits 1.5 CPU / 896Mi) for its four
pods. Each launched app adds a pod; an App that sets no
runtime.resourcesgets the chart defaults (apps.defaultResources: 50m / 64Mi requested, up to 500m / 256Mi, for a static site; 250m / 512Mi requested, up to 2 CPU / 2Gi, for a Python app, whose coldpixi installneeds the memory). - Optional: metrics-server for per-app live CPU/memory usage in the UI (without it the UI still shows limits and restarts) and the LGTM observability pack for logs.
Read the Known limitations before a demo: access is by Keycloak group (full group paths), and users manage only their own apps (admins manage all of them).
Install
Section titled “Install”Create the namespaces apps will run in first. The chart gives the API a RoleBinding in
each namespace in api.allowedNamespaces (default apps), so they must exist, and the
nebari-operator only processes namespaces that opt in:
kubectl create namespace appskubectl label namespace apps nebari.dev/managed=trueThen install:
# From a checkout: the chart's default image tags are its appVersion, which# only matches images at a release commit. On main, use the images main builds.helm install nebari-apps charts/nebari-apps \ --namespace nebari-apps --create-namespace \ --set clusterDomain=example.ai \ --set keycloak.url=https://keycloak.example.ai/auth \ --set operator.image.tag=latest --set api.image.tag=latest \ --set ui.image.tag=latest --set mcp.image.tag=latest \ --set proxy.image.tag=latest
# The nebari-operator only processes namespaces that opt in:kubectl label namespace nebari-apps nebari.dev/managed=trueOr from the published chart with the ready-made values file
(examples/nebari-values.yaml;
edit the two hostnames):
helm install nebari-apps oci://ghcr.io/nebari-dev/apps-pack/charts/nebari-apps \ --namespace nebari-apps --create-namespace -f examples/nebari-values.yamlWith GitOps, apply the Argo CD Application in
examples/argocd-application.yaml instead: it installs the same published chart
with the same values (edit the two hostnames and targetRevision).
kubectl apply -n argocd -f examples/argocd-application.yamlThis deploys four components. A fifth image, apps-proxy, is not a Deployment: the operator adds it as a sidecar to Apps that can idle or that run an image.
| Component | Where it ends up |
|---|---|
| apps-operator | Watches App resources cluster-wide. |
| apps-api | In-cluster Service; the UI proxies /api to it same-origin. |
| apps-ui | https://apps.example.ai — with a landing-page tile (“Apps”). |
| apps-mcp | https://apps.example.ai/mcp — agent tools, proxied by the UI (see MCP server). |
Every app launched afterwards gets https://<subdomain>.apps.example.ai.
Key values
Section titled “Key values”The full reference, including every component’s values and the NebariApp fields the chart
emits, is on the Configuration page.
| Value | Default | Purpose |
|---|---|---|
clusterDomain | — (required) | The cluster’s base domain. |
appsDomain | apps.<clusterDomain> | Domain apps are served under. |
tls.enabled | true | Set false to serve plain HTTP (no certificates). |
keycloak.url | — | Browser-facing Keycloak base URL (required when auth is on). |
keycloak.realm | nebari | Keycloak realm. |
keycloak.internalUrl | — | Optional in-cluster Keycloak URL (split horizon) for JWKS. |
api.auth.enabled | true | Keycloak JWT auth for the API + keycloak-js login in the UI. |
api.allowedNamespaces | ["apps"] | Namespaces users may launch into via the API/UI/MCP. Empty list = every namespace labeled nebari.dev/managed=true. |
apps.landingPage.enabled | false | Cluster default for landing-page tiles of launched apps; apps override with spec.landingPage.enabled. |
ui.hostname | <appsDomain> | Where the UI itself is served. |
ui.title, ui.branding.* | — | Rebrand the UI (title, logos, favicon, theme, banners) — see Branding. |
ui.landingPage.* | ”Apps” tile | Landing-page tile label, description, category, icon, and health check — see Branding. |
gateway | public | Shared Gateway apps attach to (public | internal). |
staticImage | nginxinc/nginx-unprivileged:1.27-alpine | Serves static app content. |
gitImage | alpine/git:v2.47.2 | Init-container image for git sources. |
pythonImage | ghcr.io/prefix-dev/pixi:0.68.1-noble | Runs Python/pixi apps (runtime.pixiTask). |
Launch your first app
Section titled “Launch your first app”
appsis the default entry inapi.allowedNamespaces— to launch into other namespaces from the UI/API/MCP, add them to that list (or set it empty to allow every managed namespace). A namespace added to the list must exist beforehelm upgrade.kubectl applyis only gated by the namespace label.
Then either open the UI at https://apps.example.ai and use the launch form, or apply a
sample App:
kubectl apply -n apps -f examples/static-inline-app.yaml # or python-inline-app.yamlkubectl get apps -n apps -wNAME SOURCE PHASE URLdocs-site inline Running https://docs-site.apps.example.aiWhen PHASE reaches Running, open the URL. Private apps redirect to Keycloak; public apps
(access.public: true) are reachable anonymously.
Connect a coding agent
Section titled “Connect a coding agent”Point Claude Code (or any MCP client) at the cluster and launch with natural language:
claude mcp add --transport http --client-id nebari-apps-mcp --callback-port 33418 \ nebari-apps https://apps.example.ai/mcpThe client id is the Keycloak client your admin created for MCP sign-in (mcp.oauth.clientId;
the UI’s Settings page shows the exact command). Run /mcp in Claude Code, select
nebari-apps and choose Authenticate to sign in through the browser. See the
MCP server guide for the tool list and the
scaffolding skill for generating apps the agent can launch.
Verify a deployment
Section titled “Verify a deployment”# The App's own status: phase, URL, replicas, conditionskubectl describe app docs-site -n apps
# The children the operator createdkubectl get deploy,svc,cm,nebariapp -n apps
# Expected App conditions:# Validated: True - spec is coherent, namespace opted in# WorkloadReady: True - all replicas ready# RoutingReady: True - the NebariApp reports Ready (routing/TLS/auth)Deleting an App cascades: the Deployment, Service, ConfigMap, and NebariApp (and through
it the HTTPRoute, certificate, and OIDC client) are all garbage-collected.
Known limitations
Section titled “Known limitations”The pack is alpha: the happy path works, and these are the gaps to know about before a demo or pilot.
- Access is by Keycloak group, matched as full paths. A private app that lists
access.groupsis enforced at the gateway (nebari-operatorv0.2.0or later), which returns403to every other signed-in user.financemeans/finance, and a parent group does not admit members of its subgroups, so list each subgroup. Group changes apply when the user’s token refreshes (5 minutes by default). A private app with no groups is open to any signed-in realm user, and there is no per-user allow list. The gateway is the only check, so turn onapps.networkPolicy.enabledto keep other pods from reaching apps directly. See Access control. - Authorization is owner, manager or admin. The API lets a user see and manage the Apps
they launched or are listed as a manager of; members of
api.adminGroupsmanage all of them. Ownership cannot be transferred yet, and anyone withkubectlaccess to an app namespace bypasses the API. - App secrets are environment variables only. Credentials set as
secrets reach the app as env vars from its own Secret; there is
no mounted-file form yet, and no external secret store integration (Vault, External Secrets
Operator can still create an app’s
app-<name>-secrets). A Secret edited withkubectlreaches running pods only after a rollout restart. The API’s ServiceAccount can create and patch Secrets (never read them) in app namespaces, cluster-wide whenapi.allowedNamespacesis empty. - Inline sources are capped at ~900 KB of UTF-8 text (a ConfigMap). Binary assets or
larger sites need a
gitorpvcsource. - No per-user quota. Launched pods get default requests/limits and at most 10 replicas
each, but nothing caps how many Apps one user launches. Add a
ResourceQuotato app namespaces if that matters (the chart can render aLimitRangeand aNetworkPolicy:apps.limitRange/apps.networkPolicy). - Scale to zero is off by default and has cold starts. Idle Apps are scaled to zero only
when
apps.idle.timeoutMinutes(or an App’sruntime.idleTimeoutMinutes) is set. The first request after idling gets a “starting” page until a pod is ready: seconds for a static app, up to minutes for a Python app, which reinstalls its pixi environment. Any request to an idle App’s URL wakes it, including bots on public apps (the gateway still keeps users outside a restricted App’s groups out). Apps that can idle carry the proxy sidecar, and Python apps there listen on 8081. Idle detection counts HTTP requests through the sidecar only, so an app that works in the background with no visitors needsruntime.keepAlive: true. See Scale to zero. - Container images are off by default and need non-root images.
source.type: imageworks only for registries inapps.image.allowedRegistries, and the cluster never builds images. The image must run as a non-root user (root images fail withImageRunsAsRoot; nothing relaxes the policy), must not listen on 8080 or 8082 (the sidecar’s), and gets a writable root filesystem but no volumes of its own. Pull secrets come from the chart and are shared by every image App in a namespace; there is no per-App registry credential. See Container images. - One API version (
v1alpha1) and no conversion webhook yet.helm upgradedoes not update CRDs; re-applycharts/nebari-apps/crds/on upgrades (see Upgrading). - The MCP server must run as a single replica: handshake-era MCP sessions are held in process memory.
- The MCP sign-in client is created by hand. The nebari-operator cannot provision a Keycloak client with a loopback redirect URI yet; see The sign-in client.
- Local dev disables authentication (
api.auth.enabled=falsewithtls.enabled=false; the chart refuses that with TLS on unlessapi.auth.insecureAllowAnonymous=true). Never carry that flag to a real cluster.