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-clusterfrom step 1 is Running. - Cloud Shell is open (top bar Activate Cloud Shell icon) and connected to the cluster:
Expect a value likekubectl config current-context
gke_<project>_us-west1_toy-hydro-cluster. Autopilot spins nodes up only when workloads are scheduled. kubectlandhelmare 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
- Create a namespace and add the Helm repo:
kubectl create namespace jhubhelm repo add jupyterhub https://jupyterhub.github.io/helm-chart/helm repo update
- 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
EOFline 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_userscan log in. Update those lists if you need others. userScheduler.enabled: falseis required because GKE Autopilot forbids custom schedulers; we must use the default GKE scheduler.cloudMetadata.blockWithIptables: falseis required because Autopilot disallows pods that add NET_ADMIN/iptables rules; leaving ittruewould be rejected by Autopilot’s security controls.networkPolicy.egressallows the singleuser servers to reach theportalnamespace (needed for the portal service). The chart merges this rule with its own egress rules (including DNS), so nothing else is blocked.podPriority.enabled: truegives the hub pods a defined priority relative to placeholder pods (the chart creates its own priority classes for this). Disabling theprePuller.hookskips the pre-upgrade image puller, which only pre-pulls images to nodes for speed and would makehelm upgradewait 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 inJHUB_PASSfor the session. Note that JupyterHub 5.3+ deprecatesDummyAuthenticator.passwordin favor ofSharedPasswordAuthenticator; 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’svalues.yamlfor the replacement.
- 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
- Watch the pods until everything is Ready (Autopilot may need a few minutes to add nodes):
kubectl -n jhub get pods -w
Access JupyterHub
- Find the external IP of the load balancer:
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):kubectl -n jhub get svc proxy-publicNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEproxy-public LoadBalancer 10.0.32.123 8.229.165.77 80:30614/TCP 2m
- 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
- Open
https://${JHUB_HOST}/in your browser. To remember the credentials you generated earlier, print them in Cloud Shell:Sign in with the admin username you listed (printed byecho "User: ${JHUB_ADMIN}"echo "Pass: ${JHUB_PASS}"echo ${JHUB_ADMIN}) and that password. Becauseallowed_usersrestricts 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