Skip to content

Nebi integration

Nebi is the environment manager users reach from inside JupyterLab. It runs as its own service (nebari-nebi-pack, in its own namespace) and this chart wires JupyterHub to it: the binary ships into every user pod, and the hub performs a token exchange so each user’s Nebi session is scoped to them.

This page is the administrator’s side. For what users do with it once it works, see the Nebi in JupyterLab page added by PR #204.

  1. nebi-pack deployed, in its own namespace (nebi by default), from nebari-nebi-pack.
  2. Its own Keycloak OIDC client, provisioned by the operator from nebi-pack’s own NebariApp.
  3. Network reachability — the hub must reach Nebi in-cluster, and user pods must reach it through the gateway.

This chart contributes the third and the wiring; it does not deploy Nebi itself.

With keycloak.hostname set and nebi-pack installed under its default release name and namespace, nothing else is required. The defaults derive:

ValueDerived as
nebi.remoteURLhttps://<subdomains.nebi>.<base domain>
nebi.internalURLhttp://nebi-pack-nebari-nebi-pack.<nebi.namespace>.svc.cluster.local
Nebi OIDC client IDnebi-<nebi.releaseName>-nebari-nebi-pack
Hub OIDC client IDjupyterhub-<release>-<chart>

Override any of them when your layout differs:

nebi:
namespace: nebi
releaseName: nebi-pack
remoteURL: https://nebi.example.com
internalURL: http://nebi-pack-nebari-nebi-pack.nebi.svc.cluster.local
port: 8460

The Nebi binary is not baked into the JupyterLab image. An init container copies it out of nebi.image into an emptyDir mounted at /usr/local/bin/nebi in the user pod.

That means the Nebi version is a deploy-time decision:

nebi:
image:
repository: quay.io/nebari/nebi
tag: "sha-bd25921"
pullPolicy: IfNotPresent

Leaving tag empty disables the init container entirely — no Nebi in user pods. It is pinned per chart release (scripts/bump_image_tags.py only handles the JupyterLab images), so override it to test a PR build or to roll forward between chart releases.

Nebi needs a per-user credential, and the hub is the only component holding the user’s Keycloak tokens. At spawn, the hub runs a three-step exchange:

  1. Refresh the user’s access token at Keycloak, using the hub client’s ID and secret.
  2. Exchange that access token for an ID token with the Nebi audience — a standard Keycloak token-exchange grant from the hub client to the Nebi client.
  3. Exchange the Nebi ID token at Nebi’s own /api/v1/auth/session for a Nebi JWT.

The resulting JWT is injected into the user pod, so Nebi acts as that user rather than as a shared service account. The same helper is reused by jhub-apps to populate its environment dropdown.

Five inputs must all resolve or the exchange aborts:

InputSource
keycloak-token-urlderived from keycloak.hostname / serviceHost + realm
nebi-client-idderived from nebi.releaseName
jupyterhub-client-idderived from the release and chart names
JUPYTERHUB_OIDC_CLIENT_SECRETenv var from the operator-provisioned Secret
nebi-internal-urlderived from nebi.namespace

The hub logs exactly which are missing:

Terminal window
kubectl -n data-science logs deploy/hub | grep -i "token-exchange\|nebi-envs"

Two paths, two policies.

Hub → Nebi is rendered automatically whenever nebi.internalURL and nebi.namespace are both set (hub-nebi-networkpolicy.yaml), allowing egress to nebi.port in the Nebi namespace. It is needed for the token exchange at spawn time.

User pods → Nebi goes through the gateway, not the Service, because users reach Nebi at its external URL. That is what singleuser.networkPolicy.allowEgressToGateway (default true) covers — an additional NetworkPolicy permitting egress to the Envoy Gateway pod:

singleuser:
networkPolicy:
allowEgressToGateway: true
gatewayNamespace: envoy-gateway-system
gatewayName: nebari-gateway
gatewayPort: 10443

Kubernetes unions egress rules across policies selecting the same pod, so both are additive to the z2jh subchart’s own policy.

Each user gets a dedicated RWO PVC, nebi-workspaces-{slug}, created by the spawner and mounted at /var/lib/nebi/workspaces:

jupyterhub:
custom:
workspace-storage-class: "" # empty = cluster default
workspace-storage-capacity: "20Gi"

Pixi environments run 2–5 GiB each, so 20 GiB is a handful of environments, not dozens. Size it against how many environments you expect a user to keep.

Registries set here are rendered into a ConfigMap mounted at /etc/nebi/config.yaml in every user pod, which Nebi reads at boot. They appear locked in the UI rather than per-user editable:

nebi:
seedDefaultRegistry: true
registries:
- name: acme-registry
url: registry.acme.com
namespace: acme-envs
default: true

Each entry follows Nebi’s own registries.entries schema (name, url, namespace, default).

Set seedDefaultRegistry: false to remove the built-in quay.io/nebari_environments registry Nebi seeds by default.

jhub-apps offers a Nebi environment when deploying an app. Two things gate it:

  • The chart auto-injects a Nebi card into japps-config.additional_services when nebi.remoteURL is set or derivable. Overriding additional_services replaces that default — re-include it if you add your own.
  • jupyterhub.custom.jhub-app-proxy-version must be ≥ v0.2.3. Older versions only activate conda environments and silently fall back to the base environment, so an app launched into a Nebi environment comes up missing its packages.

The init container that pre-pulls Nebi environments makes its own outbound HTTPS calls (nebi pull, then pixi install from PyPI and conda). When jupyterhub.custom.trust-bundle-enabled is on, it receives the merged CA bundle too — the CA merge step is ordered before it, so the bundle is ready. See Values reference.

SymptomCheck
No Nebi in the pod at allnebi.image.tag is empty, so the init container is not wired.
Token exchange abortsHub logs name the missing input — usually nebi-client-id from a non-default nebi.releaseName.
invalid_client from KeycloakThe derived Nebi client ID does not match the client the operator created for nebi-pack.
Empty environment dropdown in jhub-appsExchange failure, or auth_state missing — kubectl logs deploy/hub | grep nebi-envs.
Registry changes not visibleHub not restarted since the change.
Apps missing packages in a Nebi envjhub-app-proxy-version below v0.2.3.
Terminal window
# Is the binary in the pod?
kubectl -n data-science exec <user-pod> -- nebi --version
# Did the workspace PVC bind?
kubectl -n data-science get pvc -l app=nebi-workspaces
# What did the hub resolve?
kubectl -n data-science logs deploy/hub | grep -i "token-exchange"