Sign inSign up

truebyteinnovationllp/jupyterhub-chart

By truebyteinnovationllp

Updated 3 months ago

Enterprise JupyterHub Helm chart (OCI) — KubeSpawner, multi-kernel, OAuth, 0-CVE hardened hub

Helm
Artifact
0

100

truebyteinnovationllp/jupyterhub-chart repository overview

JupyterHub — Enterprise Helm Chart

Artifact Hub Cosign 0 CVEs License

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.


Features

CategoryWhat's included
SpawnerKubeSpawner 7 — one Pod per user, profile-driven kernel selection
Kernel profilesPython Data Science, ML (TensorFlow/PyTorch), GPU, R (tidyverse), Julia
AuthenticationGitHub OAuth, Google OAuth, GitLab OAuth, Generic OIDC, Native (username/password), Dummy (dev)
Idle cullingAutomatic shutdown of idle servers (configurable timeout, max-age, concurrency)
DatabaseSQLite (default, dev) or PostgreSQL + asyncpg (production)
SecurityNetworkPolicy, PodDisruptionBudget, KubeSpawner RBAC, uid 65532, seccomp RuntimeDefault
Ingressnginx/traefik/any ingress class, TLS via cert-manager
Supply chainImage + chart Cosign-signed, SBOM + provenance attestations, values.schema.json
ObservabilityPrometheus ServiceMonitor (optional)

Quick Start

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.


Production Install (GitHub OAuth + PostgreSQL + Ingress)

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"

Kernel Profiles

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.

ProfileImageRAMCPU
Python — Data Sciencequay.io/jupyter/scipy-notebook:2024-10-072 GB1
Python — Machine Learningquay.io/jupyter/tensorflow-notebook:2024-10-078 GB4
Python — ML with GPUquay.io/jupyter/tensorflow-notebook:2024-10-0716 GB4 + 1 GPU
R — Statisticsquay.io/jupyter/r-notebook:2024-10-074 GB2
Julia — Scientific Computingquay.io/jupyter/julia-notebook:2024-10-074 GB2

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

Authentication

Switch the authenticator with a single value:

auth:
  type: github   # dummy | github | google | gitlab | generic-oauth | native
GitHub OAuth

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"]
Google OAuth

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
GitLab OAuth
auth:
  type: gitlab
  gitlab:
    clientId: "abc"
    clientSecret: "secret"
    callbackUrl: "https://hub.example.com/hub/oauth_callback"
    gitlabUrl: "https://gitlab.com"
    allowedGroups: ["mygroup"]
Generic OIDC (Keycloak, Okta, Auth0, Azure AD)
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"
Native (username + password)

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"]

Idle Server Culling

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

Database

SettingUse case
database.type: sqliteDevelopment, < 200 concurrent users
database.type: postgresqlProduction, 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

Scaling to 1,000s of Users

The hub itself is a singleton (one replica) — it holds CHP routing state. Scale user capacity by:

  1. Adding resource limits per profile — prevent any single user from over-consuming
  2. Increasing hub resources — 1 GB RAM is comfortable for ~200 users; 2 GB for 1,000+
  3. Enabling culling — free up cluster resources from idle servers
  4. Using PostgreSQL — SQLite serializes writes; use PostgreSQL for > 200 concurrent sessions
  5. Node pools / taints — dedicate GPU nodes with spawner.userNodeSelector + userTolerations
hub:
  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

Persistence

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"

Extra Configuration

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'}

Security

Image hardening

The hub runs truebyteinnovationllp/jupyterhub-k8s:5.5.0:

  • Debian 13 — all OS packages upgraded at build time
  • uid/gid 65532 (nonroot) — works with restricted PodSecurityAdmission
  • No package manager (apt/apt-get removed from runtime layer)
  • No perl — binaries and dpkg entry deleted; 0 perl CVEs
  • 0 OS CVEs — verified by Docker Scout + Trivy on every release
Pod security
podSecurityContext:
  runAsNonRoot: true
  runAsUser: 65532
  seccompProfile:
    type: RuntimeDefault

containerSecurityContext:
  allowPrivilegeEscalation: false
  capabilities:
    drop: ["ALL"]
Network isolation
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
Verifying the chart signature
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

Values Reference

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:

SectionPurpose
imageHub image (registry, tag, digest, pullPolicy)
authAuthenticator type + provider credentials
spawnerKubeSpawner settings, profile list, storage
cullIdle server culling
databaseSQLite or PostgreSQL
secretCookie secret + crypt key (auto-generated if empty)
serviceClusterIP / LoadBalancer / NodePort
ingressIngress class, hosts, TLS
networkPolicyHub-level network isolation
persistenceHub data PVC (SQLite DB, state files)
metricsPrometheus ServiceMonitor
hub.extraConfigRaw Python appended to jupyterhub_config.py

Source & Provenance

Tag summary

Content type

Unrecognized

Digest

sha256:b0547f072

Size

118 Bytes

Last updated

3 months ago

docker pull truebyteinnovationllp/jupyterhub-chart:artifacthub.io