Blog: Data Science with Python & JupyterHub on Kubernetes - Part 1

January 6, 2021

Provisioning environments for data scientists and analysts to run simulations, test new models, or experiment with new datasets can be time-consuming and error-prone. Python is a popular choice for data science use cases, and one of the easiest ways to leverage Python is through Jupyter Notebooks. A web-based development environment for multiple languages, Jupyter Notebooks support the creation and sharing of documents that contain code, equations, visualizations, output, and markup text all in the same document. Because Jupyter Notebooks are just text files, they can be easily stored and managed in a source code repository such as GitLab or GitHub. JupyterHub, meanwhile, is a multiuser hub that spawns, manages, isolates, and proxies multiple instances of a single-user Jupyter Notebook server.

Kubernetes provides the perfect abstractions and API to automate consistent and isolated environments for data scientists to conduct their work. Combining these three things—Jupyter Notebooks, Python, and Kubernetes—into one powerful platform therefore makes a lot of sense.

In the first post in this two-part series, you will learn how to deploy a Kubernetes cluster using kind on a Mac, then how to install JupyterHub into that cluster. In the second post, we will show you how to use the data science and machine learning notebooks you have created on your newly deployed JupyterHub service running on Kubernetes.

Example notebook in JupyterHub

Before You Begin

There are a few things you’ll need before getting started with Jupyter Notebooks on Kubernetes. These instructions leverage the kind project (, which stands for Kubernetes in Docker. I ran through this example on my MacBook, but any system that has Docker installed can be used; kind will create a default storage class for your cluster using a local path persistent volume. For a production deployment of JupyterHub, you want to make sure you have persistent storage (using the CSI) that maps to a shared storage solution such as NFS, iSCSI, or FC. The default JupyterHub Helm chart settings create a service type load balancer because most managed Kubernetes offerings come configured with a cloud load balancer. To keep this tutorial brief, we will use Kubernetes port forwarding to forward a local port to our proxy-public service, which would normally be exposed through the cloud load balancer.

Install kind and Create Your Kubernetes Cluster

Start by taking the following steps:

  1. Open your favorite terminal emulator on your Mac, which should be running OS X
  2. Use the Brew package manager, which makes installing kind on Mac OS X simple
❯ brew install kind
==> Downloading
==> Downloading from
######################################################################## 100.0%
==> Pouring kind-0.9.0.catalina.bottle.1.tar.gz
==> Caveats
zsh completions have been installed to:
/usr/local/share/zsh/site-functions is not in your zsh FPATH!
Add it by following these steps:
==> Summary
🍺 /usr/local/Cellar/kind/0.9.0: 8 files, 9.2MB
==> `brew cleanup` has not been run in 30 days, running now...
Removing: /Users/ (2.0MB)
Removing: /Users/ (1.9MB)
Removing: /Users/ (13.5MB)
Removing: /Users/ (64B)
Removing: /Users/ (64B)
Removing: /Users/ (64B)
Removing: /Users/ (64B)
Removing: /Users/ (64B)
Removing: /Users/ (64B)
Removing: /Users/ (64B)
Removing: /Users/ (3 files, 172.3KB)
Pruned 1 symbolic links from /usr/local
  1. Download a kind cluster configuration manifest or create your own. This is my config:
❯ cat multinode-conf.yaml
kind: Cluster
apiServerAddress: ""
apiServerPort: 6443
- role: control-plane
- role: worker
- role: worker
  1. Create your local Kubernetes cluster:
❯ kind create cluster --config ./multinode-conf.yaml --name jupyter
Creating cluster "jupyter" ...
✓ Ensuring node image (kindest/node:v1.19.1) 🖼
✓ Preparing nodes 📦 📦 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Joining worker nodes 🚜
Set kubectl context to "kind-jupyter"
You can now use your cluster with:
kubectl cluster-info --context kind-jupyter
Have a question, bug, or feature request? Let us know! 🙂
  1. Test your Kubernetes cluster:
❯ kubectl config get-contexts
* kind-jupyter kind-jupyter kind-jupyter
❯ kubectl get nodes
jupyter-control-plane Ready master 2m31s v1.19.1
jupyter-worker Ready <none> 2m1s v1.19.1
jupyter-worker2 Ready <none> 2m1s v1.19.1

HELM Install

Before we can install the JupyterHub Helm chart, we need to ensure Helm is installed. If you don’t already have it, follow the instructions below for OS X or reference this Getting Started Guide.

On your Mac, download and install the Helm CLI if you haven’t already done so.

❯ curl -fsSL -o
❯ chmod 700
❯ ./
Verifying checksum... Done.
Preparing to install helm into /usr/local/bin
helm installed into /usr/local/bin/helm

Install JupyterHub

We are now going to leverage the Helm package manager for Kubernetes to install JupyterHub. Follow the instructions below to deploy JupyterHub notebooks on your Kubernetes cluster.

Add the Jupyter Helm chart repo to Helm:

❯ helm repo add jupyterhub
❯ helm repo update

On a Linux machine, generate a random hex string to be used as a security token by JupyterHub:

❯ openssl rand -hex 32

Confirm the existence of the “standard” storage class, which will be used for the persistent volume for each instance spawned by JupyterHub. This will allow each user to have a 10GB persistent volume so that their work will persist through restarts of their instance or pod and be mounted to their home directory.

❯ kubectl get storageclass
standard (default) Delete WaitForFirstConsumer false 3m

Modify the configuration of a Helm chart. This is done at deployment time with a values YAML file. To simplify your experience, we have already created a configuration YAML file for the JupyterHub Helm package, which you can download and edit. We have preconfigured this values file to configure several things for your JupyterHub installation:

  • Thanks to the new Docker image pull changes on Docker Hub, you will need to configure an imagePullSecret with your Docker Hub account to pull the required images. Read more about it here.
  • Change the default interface to the newer Jupyter Lab interface. This recent improvement provides some aesthetic and functional benefits, such as providing a terminal interface to your Jupyter Notebook instance to do things like git checkouts.
❯ wget
❯ vi scipy-jhub-values-kind.yaml

Change the following in the scipy-jhub-values-kind.yaml file:

  • Put in your Docker Hub account the username, email, and password.
  • Put in the generated hex string from Step 1.
# imagePullSecret is configuration to create a k8s Secret that Helm chart's pods
# can get credentials from to pull their images.
 create: true
 automaticReferenceInjection: true
 username: '<DockerHub-Username>' ## <<Change to your User Name>>
 email: '<DockerHub-Email>' ## <<Change to your Email>>
 password: '<DockerHub-Password>' ## <<Change to your Pass>>
 type: ClusterIP
 uid: 1000
 fsGid: 1000
 type: Recreate
 type: sqlite-pvc
 - ReadWriteOnce
 storage: 3Gi
 storageClassName: standard  ## <Change to your Storage Class>
 name: jupyter/scipy-notebook
 tag: latest
 limit: 5G
 guarantee: 2.5G
 defaultUrl: "/lab"
 type: dynamic
 storageClass: standard  ## <Change to your Storage Class>
 type: ClusterIP
 secretToken: "c86a373144e4e8b1341fa5661cdc70f165856ba48eb54028eb844d41f1f2aeb4d4a0cca29b9548d52cb9b4c2fb901aa00537a9d37451a6f77953add34039ca56" ## <<Change the token to your random hex string from Step 1>>

Note: We have chosen the latest version of the jupyter/scipy-notebook, which comes with all the required libraries for the machine learning and data science use cases we will demonstrate in the second post of this two-part series. Specifically, the scipy-notebook comes preinstalled with pandas, matplotlib, scikit-learn, beautifulsoup, and seaborn. For a list of the Docker images that the Jupyter team maintains, you can look here.

Create a dedicated Kubernetes namespace to house the JupyterHub Kubernetes components:

❯ kubectl create namespace jupyter

Install JupyterHub using the Helm chart and reference the values file you modified in Step 3:

❯ helm install jhub-datascience jupyterhub/jupyterhub -f scipy-jhub-values-kind.yaml -n jupyter --timeout 180s
WARNING: You should switch to ""
NAME: jhub-datascience
LAST DEPLOYED: Thu Oct 29 13:58:01 2020
NAMESPACE: jupyter
STATUS: deployed
Thank you for installing JupyterHub!
Your release is named jhub-datascience and installed into the namespace jupyter.
You can find if the hub and proxy is ready by doing:
kubectl --namespace=jupyter get pod
and watching for both those pods to be in status 'Running'.
You can find the public IP of the JupyterHub by doing:
kubectl --namespace=jupyter get svc proxy-public
It might take a few minutes for it to appear!
Note that this is still an alpha release! If you have questions, feel free to
1. Read the guide at
2. Chat with us at
3. File issues at
directly instead.

Note: This installation will take a while to pull down all the images required to use JupyterHub and bootstrap the installation process.

You can monitor and troubleshoot the JupyterHub installation by watching the pod creations in the jupyter namespace.

❯ kubectl get po -n jupyter -w
continuous-image-puller-4lpbv 1/1 Running 0 90s
continuous-image-puller-cw8vd 1/1 Running 0 90s
continuous-image-puller-hr75f 1/1 Running 0 90s
continuous-image-puller-skspt 1/1 Running 0 90s
hub-7d9698c5d4-dh4ld 1/1 Running 0 90s
proxy-7c5f54cb77-t8l4d 1/1 Running 0 90s
user-scheduler-67f756d5d6-4gb6b 1/1 Running 0 90s
user-scheduler-67f756d5d6-f4z4c 1/1 Running 0 90s

You can also monitor for errors by putting a watch on Kubernetes events happening during the installation of the Helm release inside the jupyter namespace.

❯ kubectl get events -n jupyter -w
0s Normal Pulled pod/continuous-image-puller-6ksn9 Container image "" already present on machine
0s Normal Created pod/continuous-image-puller-6ksn9 Created container pause
0s Normal Started pod/continuous-image-puller-6ksn9 Started container pause
0s Normal EnsuringLoadBalancer service/proxy-public Ensuring load balancer
0s Normal EnsuredLoadBalancer service/proxy-public Ensured load balancer
0s Warning FailedScheduling pod/hub-7b9c7b96d5-2l4sd running "VolumeBinding" filter plugin for pod "hub-7b9c7b96d5-2l4sd": pod has unbound immediate PersistentVolumeClaims
0s Warning ProvisioningFailed persistentvolumeclaim/hub-db-dir "standard" not found

To look at a failed Helm release in Kubernetes:

❯ helm list -A
WARNING: "" is deprecated for "stable" and will be deleted Nov. 13, 2020.
WARNING: You should switch to ""
jhub-scipy jupyter 1 2020-11-04 09:25:17.906053349 -0800 PST failed jupyterhub-0.10.0 1.2.0

To uninstall a failed Helm release in Kubernetes:

❯ helm uninstall jhub-scipy -n jupyter
WARNING: "" is deprecated for "stable" and will be deleted Nov. 13, 2020.
WARNING: You should switch to ""
release "jhub-scipy" uninstalled

Jupyter Hub Access

To verify that your Jupyter Hub deployment is successful, the following Kubernetes objects should be running in the jupyter namespace:

❯ kubectl get deploy,po,svc,pvc -n jupyter
deployment.apps/hub 1/1 1 1 3m
deployment.apps/proxy 1/1 1 1 3m
deployment.apps/user-scheduler 2/2 2 2 3m
pod/continuous-image-puller-4lpbv 1/1 Running 0 3m
pod/continuous-image-puller-cw8vd 1/1 Running 0 3m
pod/continuous-image-puller-hr75f 1/1 Running 0 3m
pod/continuous-image-puller-skspt 1/1 Running 0 3m
pod/hub-7d9698c5d4-dh4ld 1/1 Running 0 3m
pod/proxy-7c5f54cb77-t8l4d 1/1 Running 0 3m
pod/user-scheduler-67f756d5d6-4gb6b 1/1 Running 0 3m
pod/user-scheduler-67f756d5d6-f4z4c 1/1 Running 0 3m
hub ClusterIP <none> 8081/TCP 3m
proxy-api ClusterIP <none> 8001/TCP 3m
proxy-public ClusterIP <none> 80/TCP 3m
claim-admin Bound pvc-46f13b00-580a-435a-b263-22b4cfd2376d 10Gi RWO
standard 20m
hub-db-dir Bound pvc-96243b44-2088-44df-828a-d9abf75cea46 3Gi RWO standard 51m

The final step is to expose the proxy-public Kubernetes service locally on your Mac using port forwarding.

❯ kubectl port-forward -n jupyter svc/proxy-public 8080:80 &
[2] 39859
Forwarding from -> 8000
Forwarding from [::1]:8080 -> 8000

To access the JupyterHub web UI from your Mac, use your web browser and enter your local IP address with the port specified above (8080), so http://localhost:8080/.

JupyterHub login screen

Congratulations! You are now ready to consume the service you have created and developed using Python Jupyter Notebooks running on JupyterHub on Kubernetes. In Part 2 of this series, we will focus on how to use JupyterHub and use Jupyter Notebooks on Kubernetes.

Blog: Data Science with Python & JupyterHub on Kubernetes - Part 2
Blog: Data Science with Python & JupyterHub on Kubernetes - Part 2

In a previous post, we discussed the advantages of running JupyterHub on Kubernetes. We also showed you how...

Nov 17 - APJ Greenplum Summit 2020: An introduction to Greenplum
Nov 17 - APJ Greenplum Summit 2020: An introduction to Greenplum