JupyterHub
JupyterHub provides multi-user notebook servers for teams, classrooms, and research platforms. The HelmForge chart deploys the Hub and configurable-http-proxy with KubeSpawner-oriented defaults.
Key Features
- Hub and configurable-http-proxy workloads with managed proxy token Secret
- KubeSpawner configuration for user notebook pods
- Optional user PVCs and single-user image profiles
- Namespaced RBAC for user pods
- Ingress, Gateway API, dual-stack Service fields, NetworkPolicy, ServiceMonitor, PDB, and External Secrets
- Guardrails for Hub replica count, persistence, and cookie secret behavior
Installation
helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install jupyterhub helmforge/jupyterhub --namespace jupyterhub --create-namespace
helm install jupyterhub oci://ghcr.io/helmforgedev/helm/jupyterhub --namespace jupyterhub --create-namespace
Chart References
- Chart standards PR
- Design
- Production guide
- Networking guide
- External Secrets guide
- Production values example
- Gateway API example
Examples
Single-user image:
singleuser:
image:
name: quay.io/jupyter/scipy-notebook
tag: '2026-05-26'
Profiles:
singleuser:
profiles:
- display_name: Standard
slug: standard
default: true
kubespawner_override:
cpu_limit: 1
mem_limit: 2G
Listener and Service Ports
JupyterHub has three distinct process listeners. containerPorts.hub configures the Hub bind URL,
containerPorts.proxy configures the public configurable-http-proxy listener, and
containerPorts.proxyApi configures the private proxy control API. The corresponding service.* values are
Kubernetes Service ports and may be changed independently because the Services use named target ports.
| Purpose | Process listener | Service port | Default listener / Service |
|---|---|---|---|
| Hub | containerPorts.hub |
service.hubPort |
8081 / 8081 |
| Public proxy | containerPorts.proxy |
service.port |
8000 / 80 |
| Proxy control API | containerPorts.proxyApi |
service.proxyApiPort |
8001 / 8001 |
To move the actual application listeners while preserving stable Service ports:
containerPorts:
hub: 18081
proxy: 18000
proxyApi: 18001
service:
port: 80
hubPort: 8081
proxyApiPort: 8001
The public proxy and proxy API listener ports must differ because both sockets are opened by the same proxy
container. Changing only service.port, service.hubPort, or service.proxyApiPort does not change the process
listener.
Operations
Keep the default SQLite Hub database to a single Hub replica. For HA Hub deployments, configure an external database, provide a shared cookie secret, and avoid single-writer PVC modes for shared Hub state.
Security Scan
| Framework | Score |
|---|---|
| MITRE + NSA + SOC2 | 81.666664% |
The chart runs Hub and proxy containers as non-root, drops Linux capabilities, keeps single-user pod service account tokens disabled through KubeSpawner configuration, and requires explicit opt-ins for insecure public authentication and anonymous public metrics.
Architecture
The chart deploys a JupyterHub Hub, configurable-http-proxy, and RBAC for KubeSpawner-managed user pods. The Hub owns authentication, spawning decisions, and user state. The proxy receives browser traffic and routes each authenticated user to the correct notebook server.
Main runtime pieces:
- Public traffic reaches configurable-http-proxy through Ingress or Gateway API.
- The Hub authenticates users and instructs KubeSpawner to create notebook pods.
- User notebook pods run with the configured single-user image and optional per-user storage.
- The proxy routes each user to their notebook server.
- Optional metrics and ServiceMonitor expose Hub and proxy observability.
Production Values
For production, replace DummyAuthenticator, use durable storage, configure a real authenticator, and keep metrics private:
auth:
type: custom
hub:
extraConfig: |
c.JupyterHub.authenticator_class = "nativeauthenticator.NativeAuthenticator"
# Configure c.JupyterHub.db_url before increasing hub.replicaCount.
persistence:
enabled: true
storageClass: fast-retain
size: 20Gi
singleuser:
storage:
enabled: true
storageClass: fast-retain
capacity: 20Gi
profiles:
- display_name: Standard Lab
slug: standard
default: true
kubespawner_override:
default_url: /lab
metrics:
authenticatePrometheus: true
serviceMonitor:
enabled: false
Do not expose the default dummy authentication mode on a public route. It is useful for local validation only.
Hub Database And HA
The default Hub state uses SQLite on the Hub PVC. Keep hub.replicaCount=1 in that mode. Before increasing replicas:
- Configure an external database with
c.JupyterHub.db_urlinhub.extraConfig. - Provide a stable shared cookie secret.
- Avoid single-writer PVC modes for state shared by multiple Hub replicas.
- Validate login, spawn, stop, and resume flows behind your load balancer.
For HA Hubs without Hub persistence, create a Secret containing the hex-encoded JupyterHub cookie secret:
hub:
cookieSecret:
existingSecret: jupyterhub-cookie-secret
persistence:
enabled: false
Without a shared cookie secret, login sessions can fail when requests move between Hub replicas.
Single-User Profiles
Profiles give platform teams controlled notebook options:
singleuser:
image:
name: quay.io/jupyter/scipy-notebook
tag: '2026-05-26'
profiles:
- display_name: Data Science
slug: data-science
default: true
kubespawner_override:
cpu_limit: 2
mem_limit: 4G
- display_name: Lightweight
slug: lightweight
kubespawner_override:
cpu_limit: 1
mem_limit: 1G
Use profile overrides to limit CPU, memory, GPU access, default URL, images, and storage behavior.
Networking
Ingress example:
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: hub.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: jupyterhub-tls
hosts:
- hub.example.com
Gateway API example:
gateway:
enabled: true
parentRefs:
- name: public
namespace: gateway-system
hostnames:
- hub.example.com
Enable NetworkPolicy once you have confirmed the CNI enforces it and user pods can still reach the Hub and proxy.
Secrets
The proxy token can be chart-generated for local environments. Production should use an existing Secret or External Secrets Operator:
proxy:
existingSecret: jupyterhub-proxy-token
externalSecrets:
enabled: true
secretStoreRef:
name: platform-secrets
kind: ClusterSecretStore
data:
- secretKey: proxy-token
remoteRef:
key: jupyterhub/proxy
property: proxy-token
Validation
After deployment:
helm test jupyterhub -n jupyterhub
kubectl get pods -n jupyterhub -l app.kubernetes.io/name=jupyterhub
kubectl logs -n jupyterhub deploy/jupyterhub-hub --since=10m
kubectl get events -n jupyterhub --sort-by=.lastTimestamp
Also validate a full user workflow: login, spawn a notebook, restart the Hub, and confirm the notebook session survives
when hub.cleanupServers=false.
Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
| Login sessions fail behind multiple Hubs | Cookie secret is not shared | Set hub.cookieSecret.existingSecret. |
| HA Hub render fails | SQLite DB or RWO persistence is incompatible | Configure external DB and compatible persistence. |
| ServiceMonitor cannot scrape | Prometheus auth remains enabled | Use private scrape credentials or explicitly disable metrics auth for private paths. |
| User pods stay pending | RBAC, quotas, storage class, or image pulls fail | Describe the user pod and check namespace events. |
Values
| Parameter | Default | Description |
|---|---|---|
containerPorts.hub |
8081 |
Actual Hub process listener. |
containerPorts.proxy |
8000 |
Actual public proxy process listener. |
containerPorts.proxyApi |
8001 |
Actual proxy control API listener. |
hub.replicaCount |
1 |
Number of Hub replicas. |
hub.persistence.enabled |
true |
Persist Hub state. |
proxy.secretToken |
"" |
Inline proxy token, generated when empty. |
proxy.existingSecret |
"" |
Existing Secret for proxy token. |
singleuser.image.name |
quay.io/jupyter/base-notebook |
Default notebook image. |
singleuser.storage.enabled |
false |
Create user notebook PVCs. |
service.port |
80 |
Public proxy Service port. |
service.hubPort |
8081 |
Internal Hub Service port. |
service.proxyApiPort |
8001 |
Internal proxy API Service port. |
service.ipFamilyPolicy |
null |
Optional Service dual-stack policy. |
gateway.enabled |
false |
Render Gateway API HTTPRoute. |
networkPolicy.extraEgress |
[] |
Additional Hub NetworkPolicy egress rules. |
metrics.serviceMonitor.enabled |
false |
Render ServiceMonitor. |
externalSecrets.enabled |
false |
Render ExternalSecret resources. |