Skip to main content

2 - Deploy JupyterHub with the Zero to JupyterHub chart

Goal: install JupyterHub on the GKE Autopilot cluster created in step 1, using the official Helm chart from the Zero to JupyterHub project.

Prerequisites​

  • The toy-hydro-cluster from step 1 is Running.
  • Cloud Shell is open (top bar Activate Cloud Shell icon) and connected to the cluster:
    kubectl config current-context
    Expect a value like gke_<project>_us-west1_toy-hydro-cluster. Autopilot spins nodes up only when workloads are scheduled.
  • kubectl and helm are available in Cloud Shell (preinstalled).

Tip: Cloud Shell includes Gemini; you can ask it to run gcloud, kubectl, and helm commands for you and to interpret their output if anything fails.

Install JupyterHub​

  1. Create a namespace and add the Helm repo:
    kubectl create namespace jhub
    helm repo add jupyterhub https://jupyterhub.github.io/helm-chart/
    helm repo update
  2. Create a minimal config file for a small demo deployment. Use your current shell user as admin, allow only that user to log in, and generate a temporary password on the fly. Important: the final EOF line must start at column 1 (no spaces) or the shell will keep showing > waiting for input.
export JHUB_ADMIN="${USER}"
export JHUB_PASS="$(openssl rand -base64 20)"
cat > jhub-config.yaml <<EOF
proxy:
service:
type: LoadBalancer

hub:
config:
Authenticator:
admin_users:
- ${JHUB_ADMIN}
allowed_users:
- ${JHUB_ADMIN}
DummyAuthenticator:
password: "${JHUB_PASS}"

singleuser:
image:
name: quay.io/jupyter/scipy-notebook
tag: latest
storage:
capacity: 10Gi
cloudMetadata:
blockWithIptables: false
networkPolicy:
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: portal

scheduling:
userScheduler:
enabled: false
podPriority:
enabled: true

prePuller:
hook:
enabled: false
EOF

Notes:

  • Only the user listed in admin_users / allowed_users can log in. Update those lists if you need others.
  • userScheduler.enabled: false is required because GKE Autopilot forbids custom schedulers; we must use the default GKE scheduler.
  • cloudMetadata.blockWithIptables: false is required because Autopilot disallows pods that add NET_ADMIN/iptables rules; leaving it true would be rejected by Autopilot’s security controls.
  • networkPolicy.egress allows the singleuser servers to reach the portal namespace (needed for the portal service). The chart merges this rule with its own egress rules (including DNS), so nothing else is blocked.
  • podPriority.enabled: true gives the hub pods a defined priority relative to placeholder pods (the chart creates its own priority classes for this). Disabling the prePuller.hook skips the pre-upgrade image puller, which only pre-pulls images to nodes for speed and would make helm upgrade wait for the pull; turning it off is safe for this demo. Dummy authentication is only for this tutorial; change it before exposing to real users. The password generated above is available in JHUB_PASS for the session. Note that JupyterHub 5.3+ deprecates DummyAuthenticator.password in favor of SharedPasswordAuthenticator; it still works on the current chart (which bundles JupyterHub 5.5.4) and is the simplest option for this demo. All settings above are validated against the current chart (v4.4.4 as of Oct 2026); if a future chart version rejects a key, check the chart’s values.yaml for the replacement.
  1. Install or upgrade the release (this pulls the latest chart; the config above was validated against chart 4.4.4 / app version 5.5.4 with helm v4.3 in Cloud Shell):
helm upgrade --install jhub jupyterhub/jupyterhub \
--namespace jhub \
--values jhub-config.yaml
  1. Watch the pods until everything is Ready (Autopilot may need a few minutes to add nodes):
kubectl -n jhub get pods -w

Access JupyterHub​

  1. Find the external IP of the load balancer:
    kubectl -n jhub get svc proxy-public
    Sample output once the IP is assigned (your cluster IP, external IP, node port and age will differ; the values below are from the live walkthrough of this tutorial):
    NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    proxy-public LoadBalancer 10.0.32.123 8.229.165.77 80:30614/TCP 2m
  2. Enable HTTPS with a self-signed cert. This encrypts traffic and makes authentication safer; browsers will warn about the certificate because it’s self-signed.
# 1) Capture the load balancer IP and mint a short-lived cert for that IP
LB_IP="$(kubectl -n jhub get svc proxy-public -o jsonpath='{.status.loadBalancer.ingress[0].ip}')"
export JHUB_HOST="${LB_IP}"
openssl req -x509 -nodes -newkey rsa:2048 -days 7 \
-keyout tls.key -out tls.crt -subj "/CN=${JHUB_HOST}"

# 2) Store the cert as a TLS secret the chart can read
kubectl -n jhub delete secret jhub-selfsigned --ignore-not-found
kubectl -n jhub create secret tls jhub-selfsigned --key tls.key --cert tls.crt

# 3) Create a small overlay file enabling HTTPS and rerun Helm
cat > https-selfsigned.yaml <<EOF
proxy:
https:
enabled: true
type: secret
hosts:
- ${JHUB_HOST}
secret:
name: jhub-selfsigned
EOF

helm upgrade --install jhub jupyterhub/jupyterhub \
--namespace jhub \
--values jhub-config.yaml \
--values https-selfsigned.yaml
  1. Open https://${JHUB_HOST}/ in your browser. To remember the credentials you generated earlier, print them in Cloud Shell:
    echo "User: ${JHUB_ADMIN}"
    echo "Pass: ${JHUB_PASS}"
    Sign in with the admin username you listed (printed by echo ${JHUB_ADMIN}) and that password. Because allowed_users restricts access, only that user is authorized to log in, and inside JupyterHub that user has admin rights. Browsers will label the page "Not Secure" or show a privacy warning because the certificate is self-signed, but the traffic is still encrypted, which is sufficient for this demo scenario.

Clean up (optional)​

To remove the demo deployment:

helm uninstall jhub -n jhub
kubectl delete namespace jhub