Skip to content

Values reference

values.yaml is heavily commented and remains the source of truth. This page is the same content organized for reading, with the derivation rules and the fields whose defaults are worth knowing about.

Sections marked derived fall back to a value computed from keycloak.hostname — see Admin setup.

FieldDefaultWhat it does
keycloak.hostname""External Keycloak FQDN. The one field a fresh deploy needs; everything else derives from it.
keycloak.realmnebariRealm name.
keycloak.serviceHostkeycloak-keycloakx-http.keycloak.svc.cluster.local:8080In-cluster Keycloak service, used for the hub↔Nebi token exchange when hostname is empty.
keycloak.backchannelURL""Split-horizon OIDC — see below.

backchannelURL exists for private-VPC clusters where in-cluster CoreDNS cannot resolve the external Keycloak hostname. The hub pod cannot reach hostname at all, so token exchange over the primary URL fails.

Set it to the in-cluster URL and the hub uses it for the backchannel legs (token_url, userdata_url) while the browser keeps using hostname for authorize and end-session. The /realms/<realm> suffix is appended for you.

FieldDefaultWhat it does
subdomains.hubhubLabel prepended to the base domain to derive the hub hostname.
subdomains.nebinebiSame, for Nebi.

Rendered into the NebariApp CRD. Semantics of each CRD field are in NebariApp Integration.

FieldDefaultWhat it does
nebariapp.enabledtrueRender the NebariApp. False outside Nebari.
nebariapp.hostname"" (derived)External hub FQDN.
nebariapp.service.nameproxy-publicBackend service (created by the z2jh subchart).
nebariapp.service.port80Backend port.
nebariapp.routing.routes[{pathPrefix: /}]Sends every path to the proxy.
nebariapp.auth.enabledtrueProvision a Keycloak client.
nebariapp.auth.providerkeycloakIdentity provider.
nebariapp.auth.provisionClienttrueOperator creates the client and its Secret.
nebariapp.auth.redirectURI/hub/oauth_callbackJupyterHub’s own callback path.
nebariapp.auth.scopesopenid, profile, email, groupsThe groups scope is what shared storage and profile gating read.
nebariapp.auth.enforceAtGatewayfalseSee below.
nebariapp.auth.forwardAccessTokenfalseThe hub persists tokens to auth_state; nothing upstream needs an injected Bearer.
nebariapp.landingPage.*enabled, “JupyterHub”Landing-page card: displayName, description, icon/iconLight/iconDark, category, priority, externalUrl, healthCheck.

routing must stay present. Remove it and the operator reports RoutingNotConfigured — no HTTPRoute, no TLS listener, no reachable hub.

Not to be confused with jupyterhub.singleuser, which is the upstream passthrough.

FieldDefaultWhat it does
singleuser.networkPolicy.allowEgressToGatewaytrueRenders a NetworkPolicy letting user pods reach the Envoy Gateway pod.
singleuser.networkPolicy.gatewayNamespaceenvoy-gateway-systemWhere the gateway runs.
singleuser.networkPolicy.gatewayNamenebari-gatewayGateway name, matched on gateway.envoyproxy.io/owning-gateway-name.
singleuser.networkPolicy.gatewayPort10443Gateway port.

Required on Hetzner k3s and any cluster where kube-proxy DNATs the LoadBalancer VIP to the proxy pod IP before the subchart’s pod-level policy is evaluated — without it, user pods cannot reach https://<hub>/services/japps or the Nebi host. Kubernetes unions egress rules across policies selecting the same pod, so it is harmless where it is not needed.

In-pod idle culling, separate from the hub-level jupyterhub.cull. This one fires even when a browser tab is left open. Defaults match classic Nebari.

FieldDefaultWhat it does
singleuserCuller.kernel.cullConnectedtrueCull kernels despite open browser connections.
singleuserCuller.kernel.cullIdleTimeout900Seconds before an idle kernel is culled.
singleuserCuller.kernel.cullInterval300Check interval.
singleuserCuller.kernel.cullBusyfalseNever cull a kernel running code.
singleuserCuller.terminal.cullInactiveTimeout900Seconds before an idle terminal is culled.
singleuserCuller.terminal.cullInterval300Check interval.
singleuserCuller.server.shutdownNoActivityTimeout900Seconds after the last kernel/terminal before the server self-terminates.

The hub-level culler is jupyterhub.cull (timeout: 1800, every: 600). The two work together: the in-pod culler shuts an idle server down at 15 minutes; the hub-level culler is the backstop for servers that stop reporting activity.

Full treatment in Shared Storage.

FieldDefaultWhat it does
sharedStorage.enabledtrueMount /shared/<group> in user pods.
sharedStorage.storageClass""RWX StorageClass when nfsServer.enabled: false. Empty uses the cluster default, which must support RWX.
sharedStorage.size10GiShared PVC size.
sharedStorage.accessModes[ReadWriteMany]
sharedStorage.groups[]Allowlist; empty mounts every group from the user’s token.
sharedStorage.mountPathPrefix/sharedMount prefix in user pods.
sharedStorage.nfsServer.enabledtrueTransitional in-cluster NFS server re-exporting an RWO PVC as RWX.
sharedStorage.nfsServer.storageClass""Backing RWO class for the NFS server.
sharedStorage.nfsServer.image.*quay.io/nebari/volume-nfs:0.8-repackRepack of an abandoned upstream image.
sharedStorage.nfsServer.installClientfalseDaemonSet installing nfs-common on nodes that lack it (k3s, minimal OS images).
sharedStorage.nfsServer.nodeSelector{}Pin the NFS pod — RWO reattachment can take 30–120s when it reschedules.
sharedStorage.nfsServer.nodeAffinity{}Full affinity spec; overrides nodeSelector.
sharedStorage.nfsServer.mountOptions[]Set ["nfsvers=3"] on overlayfs nodes (kind, k3d) where the image’s NFSv4 export of / is broken.

Admin guide: Nebi integration.

FieldDefaultWhat it does
nebi.image.repositoryquay.io/nebari/nebiBinary copied into user pods by an init container.
nebi.image.tagsha-bd25921Pinned per chart release. Empty disables the init container.
nebi.image.pullPolicyIfNotPresent
nebi.remoteURL"" (derived)Browser-facing Nebi URL, used for the OIDC redirect.
nebi.internalURL"" (derived)In-cluster URL for the token-exchange path.
nebi.namespacenebiWhere nebi-pack runs; drives the NetworkPolicy and derived URL.
nebi.releaseNamenebi-packDrives the derived Nebi OIDC client ID.
nebi.port8460Used in the hub→Nebi egress rule.
nebi.seedDefaultRegistrytrueSeed quay.io/nebari_environments in each user’s Nebi.
nebi.registries[]Admin-provisioned public OCI registries.

The post-install Keycloak Job — see Admin setup.

FieldDefaultWhat it does
rbac.bootstrap.enabledtrueRun the Job. False for BYO-Keycloak or local dev.
rbac.bootstrap.namespacekeycloakNamespace the Job runs in, so it can read the admin Secret without a cross-namespace copy.
rbac.bootstrap.kcAdminCredentialSecretkeycloak-admin-credentialsSecret holding the realm-admin password. Unset makes the Job skip cleanly.
rbac.bootstrap.kcAdminCredentialSecretKeyadmin-passwordKey within that Secret.
rbac.bootstrap.realmNamenebariRealm to bootstrap.
rbac.bootstrap.hubClientId""Empty reads it at runtime from the operator-provisioned OIDC Secret.
rbac.bootstrap.oidcClientSecretName""Empty derives <fullname>-oidc-client.
rbac.bootstrap.sharedMountRoleNameallow-group-directory-creation-roleMust match the hub’s KC_SHARED_MOUNT_ROLE.
rbac.bootstrap.sharedMountGroups[]Keycloak group paths granted the role. Each must already exist.
rbac.bootstrap.hubExternalUrl""Empty defaults to https://{nebariapp.hostname}.
rbac.bootstrap.kcHostin-cluster Keycloak URLWhere the Admin REST API lives.
rbac.bootstrap.imagepython:3.12-slimRuns a stdlib-only script; any small Python image works.

Read by the Python files in jupyterhub_config.d/ through get_chart_config(). Every URL and client ID here is optional — leave it empty and the chart derives it. Explicit values always win.

FieldDefaultWhat it does
external-url"" (derived)Hub bind hostname.
nebi-image"" (derived)repository:tag copied into user pods.
nebi-image-pull-policyIfNotPresent
jhub-app-proxy-versionv0.2.3Installed at app-spawn time. Must be ≥ v0.2.3 for apps to run inside a Nebi (pixi) environment; older versions only activate conda and fall back to the base env.
nebi-remote-url"" (derived)Browser-facing Nebi URL.
nebi-internal-url"" (derived)In-cluster Nebi URL.
keycloak-token-url"" (derived)Token endpoint for hub↔Nebi exchange.
keycloak-backchannel-issuer-url"" (derived)Full backchannel issuer including /realms/<realm>.
nebi-client-id"" (derived)
jupyterhub-client-id"" (derived)
trust-bundle-enabledfalseMerge an org CA into user pods — see below.
trust-bundle-configmapnebari-trust-bundleConfigMap holding the org CA.
trust-bundle-keyca-certificates.crtKey within it.
profilestwo profilesServer sizes — see Server profiles.
terminal-customizationtrueStarship prompt in JupyterLab terminals.
shared-storage-groups[]Allowlist; empty = every group in the token.
shared-storage-mount-prefix/shared
storage-capacity20GiPer-user home PVC size (claim-{username}, RWO).
workspace-storage-class""Class for the per-user Nebi workspace PVC. Empty uses the cluster default.
workspace-storage-capacity20GiPixi environments run 2–5 GiB each — size accordingly.
japps-config{hub_host: hub, service_workers: 1}Attributes set on c.JAppsConfig.

trust-bundle-enabled covers clusters behind a TLS-inspecting proxy, where NIC core’s trust-manager projects the org CA into every namespace as a ConfigMap. Enabling it merges that CA with the image’s system bundle via an init container and sets REQUESTS_CA_BUNDLE, SSL_CERT_FILE, NODE_EXTRA_CA_CERTS, CURL_CA_BUNDLE, and GIT_SSL_CAINFO on singleuser and app pods — so pip, conda, and git work without --trusted-host or ssl_verify flags.

The hub-side equivalent is always on and lives in jupyterhub.hub.extraVolumes, extraVolumeMounts, initContainers, and extraEnv (search values.yaml for merge-ca-bundle). It mounts the ConfigMap optional: true, so it is a no-op where trust-manager is absent.

The trust-bundle-configmap and trust-bundle-key values drive both sides, but the hub-side entries also hardcode the ConfigMap name in jupyterhub.hub.extraVolumes. Renaming the bundle means editing both places.

Everything else goes verbatim to the Zero to JupyterHub chart, version 4.4.0. Values this chart sets that are worth knowing about:

FieldSet toWhy
jupyterhub.hub.imagequay.io/nebari/nebari-data-science-pack-jupyterhubNebari hub image with jhub-apps pre-installed.
jupyterhub.hub.config.JupyterHub.authenticator_classdummyLocal development. Real OAuth is wired by 00-gateway-auth.py from the mounted OIDC Secret.
jupyterhub.hub.config.JupyterHub.admin_accesstrue
jupyterhub.hub.service.extraPorts10202jhub-apps.
jupyterhub.singleuser.imagequay.io/nebari/nebari-data-science-pack-jupyterlab
jupyterhub.singleuser.defaultUrl/lab
jupyterhub.singleuser.storage.typenonejhub-apps’ JHubSpawner expects volumes as a list; the subchart’s dynamic storage generates a dict. The home PVC is configured in 01-spawner.py instead.
jupyterhub.proxy.service.typeClusterIPRouting is the NebariApp’s job.
jupyterhub.scheduling.userScheduler.enabledfalse
jupyterhub.cullenabled: true, timeout: 1800, every: 600Matches classic Nebari.

jupyterhub.singleuser.extraEnv is a dict, so adding a key merges rather than replaces. That is the supported hook for injecting things like MLFLOW_TRACKING_URI — see MLflow integration.

z2jh defaults singleuser.networkPolicy.enabled: true with egressAllowRules.privateIPs: false, so user pods cannot reach other in-cluster services by default. Reaching an in-cluster endpoint takes an explicit rule:

jupyterhub:
singleuser:
networkPolicy:
egress:
- ports:
- port: <pod port>
protocol: TCP
to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: <namespace>

Entries under egress are rendered verbatim into the generated policy and unioned with the built-in rules. The port must be the pod port, not the Service port — NetworkPolicy is evaluated after kube-proxy has already translated the ClusterIP.

Terminal window
helm template data-science-pack . --set keycloak.hostname=keycloak.example.com | less
helm -n data-science get values data-science-pack
helm -n data-science get values data-science-pack --all

To check a derived value actually landed, read it off the running hub rather than the values:

Terminal window
kubectl -n data-science get cm nebari-data-science-pack-hub-config -o yaml | head -40