Skip to content

Server profiles

Profiles are the server sizes users pick from at spawn. Each entry in jupyterhub.custom.profiles maps directly to a KubeSpawner profile_list item, so anything KubeSpawner accepts works without a chart change.

The chart ships two:

ProfileSlugResources
Small Instance (default)small-instance1 CPU / 2 GB limit, 0.5 CPU / 1 GB guarantee
Medium Instancemedium-instance4 CPU / 8 GB limit, 2 CPU / 4 GB guarantee

Set profiles: [] to remove the selector entirely and run in single-instance mode.

jupyterhub:
custom:
profiles:
- slug: large-instance
display_name: "Large Instance"
description: "16 CPU / 64 GB RAM — large in-memory datasets."
kubespawner_override:
image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-16c1922
cpu_limit: 16
cpu_guarantee: 8
mem_limit: "64G"
mem_guarantee: "32G"

slug is a stable identifier independent of the human-facing display_name; omit it and KubeSpawner slugifies the display name ("Large Instance"large-instance). Set it explicitly — the slug is what access: keycloak gating matches on, and renaming a display name would otherwise silently change it.

default: true marks the pre-selected profile. Exactly one should have it.

cpu_guarantee and mem_guarantee become the pod’s requests; cpu_limit and mem_limit become its limits. Guarantees drive scheduling, so a guarantee larger than any node can satisfy leaves the server Pending forever with no message in the UI.

kubespawner_override accepts any KubeSpawner trait — node_selector, image, extra_resource_limits, tolerations, environment, and the rest.

profile_options adds a second dropdown under the selected profile:

- slug: small-instance
display_name: "Small Instance"
default: true
kubespawner_override:
image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-16c1922
cpu_limit: 1
mem_limit: "2G"
profile_options:
image:
display_name: Image
choices:
default:
display_name: "nebari-data-science-pack-jupyterlab:sha-16c1922"
default: true
kubespawner_override:
image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-16c1922
rlang:
display_name: "R"
kubespawner_override:
image: quay.io/nebari/nebari-data-science-pack-jupyterlab-r:sha-16c1922

Each profile can declare an access mode controlling who sees it. This is parity with classic Nebari.

accessVisible to
all (or omitted)everyone
yamlusers whose Keycloak groups intersect groups, or whose preferred_username is in users
keycloakusers whose jupyterlab-profiles Keycloak role lists this profile’s slug
- slug: gpu-instance
display_name: "G4 GPU Instance"
access: yaml
groups:
- gpu-access
users:
- alice
kubespawner_override:
extra_resource_limits:
nvidia.com/gpu: 1

The access, groups, and users keys are gating-only — they are stripped before the profile reaches KubeSpawner.

Moves the allow-list out of the values file and into Keycloak, which is what you want when the people granting access are not the people editing Helm values.

Create a jupyterlab-profiles client role on the hub client with:

  • attribute profiles — the allowed slugs
  • attribute component=jupyterhub-profiles

then assign the role to users or groups. The authenticator resolves it at login through the Keycloak Admin API and stamps the result into auth_state, where the spawner reads it.

Note this requires the hub client’s service account to hold the realm-management view roles — which is exactly what the Keycloak bootstrap Job provisions.

Both yaml gating and shared storage read the user’s Keycloak groups from the groups claim. If that claim is empty, access: yaml profiles are invisible to everyone and shared directories do not mount.

The usual cause is a missing oidc-group-membership-mapper on the groups client scope — which is the first thing the bootstrap Job fixes. Check with:

Terminal window
kubectl -n data-science logs deploy/hub | grep -i "profiles:\|groups"

A GPU profile requests the resource through extra_resource_limits, but scheduling onto a tainted GPU node group also needs a toleration — whether you must add it yourself depends on whether the cluster runs the ExtendedResourceToleration admission controller (EKS and GKE do; AKS and most self-managed clusters do not).

That, plus the fact that tolerations in kubespawner_override replaces rather than appends, is covered in detail on the GPU profiles page added by PR #139.

Two independent cullers, both on by default.

ValueDefaultScope
In-podsingleuserCuller.*15 minKernels, terminals, and the server itself — fires even with a browser tab open
Hub-leveljupyterhub.cull30 minServers the hub sees as inactive

The in-pod culler is the one that actually reclaims resources from users who leave a tab open overnight; the hub-level culler is the backstop. Raising one without the other rarely does what you want — see Values reference.

Each user gets two PVCs, both ReadWriteOnce:

PVCSize valueDefaultContents
claim-{username}jupyterhub.custom.storage-capacity20GiHome directory, mounted at /home/jovyan
nebi-workspaces-{slug}jupyterhub.custom.workspace-storage-capacity20GiNebi (pixi) environments

Set workspace-storage-class and let storage-capacity follow your home-directory policy. Pixi environments run 2–5 GiB each, so the workspace PVC fills faster than people expect.

Profile changes live in the hub ConfigMap, so the hub must restart:

Terminal window
kubectl -n data-science rollout restart deployment/hub

Running servers keep the profile they spawned with. Users see the new list on their next spawn.