Enterprise JupyterHub Helm chart (OCI) — KubeSpawner, multi-kernel, OAuth, 0-CVE hardened hub
100
Enterprise-grade, multi-kernel JupyterHub for Kubernetes. Designed for teams of 1,000s of concurrent data scientists, ML engineers, and researchers.
Built on a hardened 0-CVE image (truebyteinnovationllp/jupyterhub-k8s:5.5.0) running on Debian 13, uid 65532 (nonroot), with no package manager in the runtime layer.
| Category | What's included |
|---|---|
| Spawner | KubeSpawner 7 — one Pod per user, profile-driven kernel selection |
| Kernel profiles | Python Data Science, ML (TensorFlow/PyTorch), GPU, R (tidyverse), Julia |
| Authentication | GitHub OAuth, Google OAuth, GitLab OAuth, Generic OIDC, Native (username/password), Dummy (dev) |
| Idle culling | Automatic shutdown of idle servers (configurable timeout, max-age, concurrency) |
| Database | SQLite (default, dev) or PostgreSQL + asyncpg (production) |
| Security | NetworkPolicy, PodDisruptionBudget, KubeSpawner RBAC, uid 65532, seccomp RuntimeDefault |
| Ingress | nginx/traefik/any ingress class, TLS via cert-manager |
| Supply chain | Image + chart Cosign-signed, SBOM + provenance attestations, values.schema.json |
| Observability | Prometheus ServiceMonitor (optional) |
helm install jupyterhub oci://registry-1.docker.io/truebyteinnovationllp/jupyterhub-chart \
--version 0.1.0 \
--namespace jupyterhub --create-namespace \
--set auth.type=dummy
Then port-forward and open the browser:
kubectl port-forward -n jupyterhub svc/jupyterhub 8080:80
open http://localhost:8080
Log in with any username and password. Select a kernel profile. Your server starts in its own Pod.
helm install jupyterhub oci://registry-1.docker.io/truebyteinnovationllp/jupyterhub-chart \
--version 0.1.0 \
--namespace jupyterhub --create-namespace \
--set auth.type=github \
--set auth.github.clientId="YOUR_GITHUB_CLIENT_ID" \
--set auth.github.clientSecret="YOUR_GITHUB_CLIENT_SECRET" \
--set auth.github.callbackUrl="https://hub.example.com/hub/oauth_callback" \
--set auth.github.allowedOrgs[0]="your-github-org" \
--set auth.adminUsers[0]="your-github-username" \
--set database.type=postgresql \
--set database.postgresql.host="postgres.default.svc.cluster.local" \
--set database.postgresql.name=jupyterhub \
--set database.postgresql.username=jupyterhub \
--set database.postgresql.password="YOUR_DB_PASSWORD" \
--set ingress.enabled=true \
--set ingress.className=nginx \
--set "ingress.hosts[0].host=hub.example.com" \
--set "ingress.hosts[0].paths[0].path=/" \
--set "ingress.hosts[0].paths[0].pathType=Prefix" \
--set "ingress.tls[0].secretName=hub-tls" \
--set "ingress.tls[0].hosts[0]=hub.example.com"
The chart ships five kernel environments out of the box. Users select their environment when starting a server. Each profile spawns a separate Kubernetes Pod with its own resource envelope.
| Profile | Image | RAM | CPU |
|---|---|---|---|
| Python — Data Science | quay.io/jupyter/scipy-notebook:2024-10-07 | 2 GB | 1 |
| Python — Machine Learning | quay.io/jupyter/tensorflow-notebook:2024-10-07 | 8 GB | 4 |
| Python — ML with GPU | quay.io/jupyter/tensorflow-notebook:2024-10-07 | 16 GB | 4 + 1 GPU |
| R — Statistics | quay.io/jupyter/r-notebook:2024-10-07 | 4 GB | 2 |
| Julia — Scientific Computing | quay.io/jupyter/julia-notebook:2024-10-07 | 4 GB | 2 |
Kernel images are pulled on demand — they are not included in this chart's hub image. Pin image tags to digests in production.
Add or replace profiles via spawner.profileList in your values file:
spawner:
profileList:
- display_name: "My Custom Env"
description: "Company-standard Python with internal libraries"
slug: "custom"
default: true
kubespawner_override:
image: "myregistry.example.com/my-notebook:v1.2.3"
mem_limit: "4G"
mem_guarantee: "1G"
cpu_limit: 2
cpu_guarantee: 0.5
Switch the authenticator with a single value:
auth:
type: github # dummy | github | google | gitlab | generic-oauth | native
Register at https://github.com/settings/applications/new (callback: https://hub.example.com/hub/oauth_callback).
auth:
type: github
github:
clientId: "abc123"
clientSecret: "secret"
callbackUrl: "https://hub.example.com/hub/oauth_callback"
allowedOrgs: ["my-org"] # restrict to org members
allowedTeams: ["my-org:devs"] # restrict to team members
adminUsers: ["alice", "bob"]
Register at https://console.cloud.google.com/apis/credentials.
auth:
type: google
google:
clientId: "xxx.apps.googleusercontent.com"
clientSecret: "secret"
callbackUrl: "https://hub.example.com/hub/oauth_callback"
hostedDomain: "mycompany.com" # restrict to one Google Workspace domain
auth:
type: gitlab
gitlab:
clientId: "abc"
clientSecret: "secret"
callbackUrl: "https://hub.example.com/hub/oauth_callback"
gitlabUrl: "https://gitlab.com"
allowedGroups: ["mygroup"]
auth:
type: generic-oauth
genericOidc:
clientId: "abc"
clientSecret: "secret"
issuerUrl: "https://keycloak.example.com/realms/myrealm"
callbackUrl: "https://hub.example.com/hub/oauth_callback"
usernameClaim: "preferred_username"
For clusters without an external identity provider. Admin approves signups.
auth:
type: native
native:
openSignup: false # admins create accounts; true = self-registration
minimumPasswordLength: 12
adminUsers: ["alice"]
Users' servers are automatically stopped after cull.timeout seconds of inactivity:
cull:
enabled: true
timeout: 3600 # 1 hour idle → server stopped
every: 600 # check every 10 minutes
maxAge: 86400 # 0 = no limit; 86400 = 24-hour max session
concurrency: 10 # parallel stop operations
| Setting | Use case |
|---|---|
database.type: sqlite | Development, < 200 concurrent users |
database.type: postgresql | Production, 200+ concurrent users |
database:
type: postgresql
postgresql:
host: "postgres.default.svc.cluster.local"
port: 5432
name: jupyterhub
username: jupyterhub
password: "changeme" # or use existingSecret
existingSecret: "" # key: db-password
The hub itself is a singleton (one replica) — it holds CHP routing state. Scale user capacity by:
spawner.userNodeSelector + userTolerationshub:
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
spawner:
concurrentSpawnLimit: 50 # burst of 50 simultaneous server starts
startTimeout: 600 # 10 min for large images to pull
The hub writes its SQLite database and runtime state to /srv/jupyterhub. A PVC is created automatically:
persistence:
enabled: true
size: 2Gi
storageClass: "fast-ssd"
Per-user home directories are created by KubeSpawner as individual PVCs (claim-{username}):
spawner:
storage:
type: dynamic
capacity: 10Gi
storageClass: "standard"
Anything not surfaced as a chart value can be set via raw Python appended to jupyterhub_config.py:
hub:
extraConfig: |
c.Spawner.default_url = '/lab'
c.JupyterHub.tornado_settings = {'max_body_size': 1048576, 'max_buffer_size': 1048576}
c.KubeSpawner.environment = {'GRANT_SUDO': 'yes'}
The hub runs truebyteinnovationllp/jupyterhub-k8s:5.5.0:
restricted PodSecurityAdmissionpodSecurityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
networkPolicy:
enabled: true
allowExternal: false # only ingress controller can reach hub
ingressNamespaceSelector:
kubernetes.io/metadata.name: ingress-nginx
ingressPodSelector:
app.kubernetes.io/name: ingress-nginx
cosign verify \
--key https://gitlab.truebyteinnovation.com/internal-docker/docker-images/-/raw/main/assets/cosign.pub \
registry-1.docker.io/truebyteinnovationllp/jupyterhub-chart:0.1.0
A complete JSON schema is embedded at values.schema.json — browse it on ArtifactHub for auto-complete and inline validation in your editor.
Key top-level sections:
| Section | Purpose |
|---|---|
image | Hub image (registry, tag, digest, pullPolicy) |
auth | Authenticator type + provider credentials |
spawner | KubeSpawner settings, profile list, storage |
cull | Idle server culling |
database | SQLite or PostgreSQL |
secret | Cookie secret + crypt key (auto-generated if empty) |
service | ClusterIP / LoadBalancer / NodePort |
ingress | Ingress class, hosts, TLS |
networkPolicy | Hub-level network isolation |
persistence | Hub data PVC (SQLite DB, state files) |
metrics | Prometheus ServiceMonitor |
hub.extraConfig | Raw Python appended to jupyterhub_config.py |
Content type
Unrecognized
Digest
sha256:b0547f072…
Size
118 Bytes
Last updated
3 months ago
docker pull truebyteinnovationllp/jupyterhub-chart:artifacthub.io