Skip to content

Launching apps

Updated 4 min read

Every launch path produces the same App custom resource — the UI and API are conveniences over one contract, so behavior is identical regardless of how an app was created.

This pack deploys two kinds of app:

  • Static sites (HTML/CSS/JS) — served by nginx.
  • Python apps — launched by a pixi task (runtime.pixiTask); the platform runs pixi install then pixi run <task>, and the task must start a server on 0.0.0.0:8080 (PORT=8080 is injected).

App kind (static vs Python) is independent of the source — any source can carry either. An app with runtime.pixiTask set runs as a Python app; otherwise its content is served statically.

SourceBest forHow it runs
inlinesmall apps (text files, ~900KB)Files carried in the App resource, mounted from a ConfigMap.
gitversion-controlled appsA non-root init container clones the repo at pod start.
pvclarger apps, existing volumesAn existing PersistentVolumeClaim mounted (static) or copied into the workspace (Python). The claim must be labeled apps.nebari.dev/mountable=true.

Open https://apps.<cluster-domain>:

  1. Launch app → name, display name, namespace, and app type (static site or Python app). For Python apps, enter the launch task — the pixi task that starts your server.
  2. Source — Upload (a .zip of your app, or a single .html file for static sites), Git, or PVC.
  3. Runtime — replicas, CPU/memory requests, environment variables.
  4. Access — public toggle, intended groups, the subdomain, and the landing-page switch.

The dashboard tracks the rollout; the app’s detail page shows conditions, live pod logs, and Kubernetes events, plus stop/start/delete.

Uploads accept a zip archive or a single .html file:

  • Static archives need an index.html at their root; Python apps (manifest has runtime.pixiTask) need a pixi.toml or pyproject.toml instead. A single top-level folder is flattened.
  • Text files only (.html, .css, .js, .json, .svg, .py, .toml, .lock, …) up to ~900KB total — the files are carried inline in the App resource and materialized as a ConfigMap-backed volume. Bigger apps or binary assets should use a git or pvc source.

Via the API:

Terminal window
curl -X POST https://apps.example.ai/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"

A Python app is the same call with runtime.pixiTask in the manifest and a zip containing at least a pixi.toml and your Python source:

Terminal window
curl -X POST https://apps.example.ai/api/v1/apps/upload \
-H "Authorization: Bearer $TOKEN" \
-F 'manifest={"name":"py-app","namespace":"apps","displayName":"Py App",
"runtime":{"pixiTask":"start"},
"access":{"public":true,"subdomain":"py-app"}}' \
-F "file=@app.zip"

The zip (or git repo / PVC) is a normal pixi project — at minimum a pixi.toml and a .py entrypoint, plus any packages and directories you need:

app.zip
├── pixi.toml # defines dependencies and tasks
├── pixi.lock # recommended: reproducible, faster installs
├── app.py
└── pkg/
└── util.py
[tasks]
start = "uvicorn app:app --host 0.0.0.0 --port ${PORT:-8080}"

The contract:

  • runtime.pixiTask names the task to run (here start). The operator runs pixi install (with --locked when a pixi.lock is present) and then pixi run <task>.
  • The task must start a server listening on 0.0.0.0:8080; PORT=8080 and HOME=/app are injected. Routing, TLS, and SSO are identical to static apps.
  • First start resolves and downloads the environment, so cold starts take longer — commit a pixi.lock and the startup probe allows up to ten minutes.

See examples/python-inline-app.yaml for a complete resource.

Terminal window
curl -X POST https://apps.example.ai/api/v1/apps \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "team-site",
"namespace": "apps",
"displayName": "Team Site",
"source": {"type": "git", "git": {"url": "https://github.com/org/site", "ref": "main", "subdir": "public"}},
"runtime": {"replicas": 1, "env": [{"name": "LOG_LEVEL", "value": "info"}]},
"access": {"public": false, "groups": ["analysts"], "subdomain": "team-site"}
}'

See the REST API reference for the full surface.

Write the App resource directly — useful for GitOps:

apiVersion: apps.nebari.dev/v1alpha1
kind: App
metadata:
name: team-site
namespace: apps
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-site

The namespace must carry the nebari.dev/managed=true label. See the App CRD reference for every field.

Coding agents (Claude Code, Codex, or any MCP client) can launch and manage apps with natural language through the apps-mcp server — launch_app is idempotent on (namespace, name), so re-launching updates instead of failing. See the MCP server page for connection and authentication details.

  • access.public: true — no authentication; anyone with the URL.
  • Private (default) — the app’s NebariApp creates a gateway SecurityPolicy: users are redirected to Keycloak and must sign in. The app itself never sees or handles auth.

Launched apps are not shown on the Nebari landing page unless they opt in — the landing page stays a curated list of platform services. Opt in per app with spec.landingPage.enabled: true (the “Show on the landing page” switch in the launch form, landing_page: true in the MCP tool, or landingPage.enabled in nebari-app.yaml); the tile uses the app’s displayName, description and thumbnail. An operator can flip the cluster default with the chart value apps.landingPage.enabled: true, after which apps opt out with enabled: false. Either way the app is reachable at its URL and listed in the Apps UI.

ActionUIAPIkubectl
Stop (scale to 0)detail page → StopPOST .../apps/{ns}/{name}/stopset spec.runtime.replicas: 0
Startdetail page → StartPOST .../apps/{ns}/{name}/startset spec.runtime.replicas: 1
Updatedetail page → EditPATCH .../apps/{ns}/{name}edit the CR
Restart (roll pods)detail page → RestartPOST .../apps/{ns}/{name}/restartkubectl rollout restart deploy/app-<name>
Logsdetail page → LogsGET .../apps/{ns}/{name}/logskubectl logs -l apps.nebari.dev/app=<name>
MetricsMetrics page / detail cardsGET .../apps/{ns}/{name}/metrics, GET /analytics/metricskubectl top pods -l apps.nebari.dev/app=<name>
Deletedetail page → Delete (type to confirm)DELETE .../apps/{ns}/{name}kubectl delete app <name>

Changing inline content rolls the pods automatically (the pod template carries a content checksum), so an update is always a clean redeploy.