Update (March 3, 2026): See the newer tutorial: Deploy nbgrader on Jetstream with a Manila exchange disk (Kubernetes)
Short differences: - This tutorial uses
ngshare(REST API exchange, no shared filesystem). - The newer tutorial uses nbgrader’s native filesystem exchange on a shared Manila RWX volume. - The newer tutorial removes thengshareservice dependency and focuses on shared storage setup.
- UPDATED 2026-09-15: The storage backend of this tutorial has been updated: Deploy ngshare on Jetstream with a Manila volume (Kubernetes). The Manila CephFS volume honors
fsGroupon RWX claims, so ngshare works without the initContainer workaround described in the Troubleshooting section below.
This tutorial shows how to deploy nbgrader on Jetstream using ngshare, a service designed to make nbgrader work on Kubernetes without a shared filesystem exchange. This is the recommended approach for Kubernetes deployments.
We will:
- Deploy
ngsharevia Helm. - Configure JupyterHub to register the ngshare service.
- Install
ngshare_exchangeandnbgraderin the singleuser image (using an existing image). - Set a placeholder course ID.
Prerequisites
- A working Kubernetes cluster on Jetstream (Magnum + Cluster API).
- JupyterHub deployed with the Helm chart.
kubectlandhelmconfigured.- This repository cloned locally.
Step 3: Enable nbgrader in the singleuser image
You need nbgrader and ngshare_exchange inside every user pod.
This tutorial uses the standard Jupyter Docker Stacks image and installs the packages at startup.
Use the template in this repo:
nbgrader/jhub-singleuser-nbgrader.yaml
Replace COURSE_ID with your course (e.g. course101).
If you want a custom image instead, see:
https://www.zonca.dev/posts/2025-12-01-custom-jupyterhub-docker-image
After updating the values, add the file to install_jhub.sh:
--values nbgrader/jhub-singleuser-nbgrader.yaml \
Re-deploy:
bash install_jhub.shIf you forget to add the values file, user pods will not have the packages installed and you will see:
WARNING: Package(s) not found: nbgrader, ngshare-exchange
Step 4: Validate in JupyterHub
In a user pod:
python -m pip show nbgrader ngshare-exchange
nbgrader listExpected output (example):
[pip show output truncated]
[ListApp | ERROR] ngshare service returned invalid status code 404.
[ListApp | ERROR] ngshare endpoint /assignments/course101 returned failure: Course not found
[ListApp | ERROR] Failed to get assignments from course course101.
[ListApp | INFO] Released assignments:
The 404 “Course not found” is expected until you create the course in the next step.
Note: running nbgrader list in a standalone test pod (not a real JupyterHub user pod) can also return a 404 from ngshare. Always validate from an actual user server.
Full pip show output is available in this repo at:
nbgrader/expected-output/pip-show-nbgrader-ngshare.txt
Step 5: Create the course and roster
Use ngshare-course-management (installed with ngshare_exchange) to create the course and add instructors/students.
Creating a course requires an admin user.
ngshare-course-management create_course course101 instructor1
ngshare-course-management add_student course101 student1After creating the course:
[ListApp | INFO] Released assignments:
Step 6: Create and release a first assignment
Initialize a course directory with example content:
nbgrader quickstart course101Expected output (example):
[QuickStartApp | INFO] Creating directory '/home/jovyan/course101'...
[QuickStartApp | INFO] Copying example from the user guide...
[QuickStartApp | INFO] Generating example config file...
[QuickStartApp | INFO] Done! The course files are located in '/home/jovyan/course101'.
Generate the assignment and release it to ngshare:
cd /home/jovyan/course101
nbgrader generate_assignment ps1
nbgrader release_assignment ps1If you want ready-made test notebooks, this repo includes a minimal set at:
nbgrader/quickstart-source/ps1/problem1.ipynbnbgrader/quickstart-source/ps1/problem2.ipynb
Copy them into your course source before generating:
cp -r /path/to/jupyterhub-deploy-kubernetes-jetstream/nbgrader/quickstart-source/ps1 /home/jovyan/course101/source/Expected output (example):
[GenerateAssignmentApp | INFO] Updating/creating assignment 'ps1': {}
[GenerateAssignmentApp | INFO] Converting notebook /home/jovyan/course101/source/./ps1/problem1.ipynb
[GenerateAssignmentApp | INFO] Converting notebook /home/jovyan/course101/source/./ps1/problem2.ipynb
[ReleaseAssignmentApp | INFO] Successfully released ps1
If nbgrader generate_assignment fails with an “old nbgrader metadata format” error, run:
cd /home/jovyan/course101
nbgrader update .Then re-run nbgrader generate_assignment ps1 --force.
If you re-run the release, add --force:
nbgrader release_assignment ps1 --forceVerify it shows up:
[ListApp | INFO] Released assignments:
[ListApp | INFO] course101 ps1
Step 7: Student workflow (fetch + submit)
Make sure the student is added to the course:
ngshare-course-management add_student course101 student1If a student is not added, they will see:
[ListApp | ERROR] ngshare service returned invalid status code 403.
[ListApp | ERROR] ngshare endpoint /assignments/course101 returned failure: Permission denied
As the student, list available assignments:
nbgrader listExpected output (example):
[ListApp | INFO] Released assignments:
[ListApp | INFO] course101 ps1
Fetch the assignment:
nbgrader fetch_assignment ps1Expected output (example):
[FetchAssignmentApp | INFO] Successfully fetched ps1. Will try to decode
[FetchAssignmentApp | INFO] Decoding: /home/jovyan/ps1/problem2.ipynb
[FetchAssignmentApp | INFO] Decoding: /home/jovyan/ps1/problem1.ipynb
[FetchAssignmentApp | INFO] Successfully decoded ps1.
Submit the assignment:
nbgrader submit ps1Expected output (example):
[SubmitApp | INFO] Source: /home/jovyan/ps1
[SubmitApp | INFO] Encoding: problem1.ipynb
[SubmitApp | INFO] Encoding: problem2.ipynb
[SubmitApp | INFO] Submitted as: course101 ps1 2026-02-04 03:03:39.734930
Step 8: Instructor workflow (collect + autograde)
As the instructor:
cd /home/jovyan/course101
nbgrader collect ps1Expected output (example):
[CollectApp | INFO] Processing 1 submissions of "ps1" for course "course101"
[CollectApp | INFO] Collecting submission: student1 ps1
[CollectApp | INFO] Decoding: /home/jovyan/course101/submitted/student1/ps1/problem1.ipynb
[CollectApp | INFO] Decoding: /home/jovyan/course101/submitted/student1/ps1/problem2.ipynb
Autograde:
nbgrader autograde ps1Expected output (example):
[AutogradeApp | INFO] SubmittedAssignment<ps1 for student1> submitted at 2026-02-04 03:03:39.734930
[AutogradeApp | INFO] Autograding /home/jovyan/course101/autograded/student1/ps1/problem1.ipynb
[AutogradeApp | INFO] Autograding /home/jovyan/course101/autograded/student1/ps1/problem2.ipynb
You may see SAWarning lines from SQLAlchemy during autograde. These are warnings (not failures) and can be ignored for this workflow.
Notes
- Use ngshare for nbgrader exchange on Kubernetes.
- Use a dedicated course ID per class (set in
nbgrader_config.py). - To manage students and instructors, use the
ngshare-course-managementCLI installed withngshare_exchangerather than the formgrader UI.
Troubleshooting
Before running exchange commands (nbgrader list, nbgrader fetch_assignment, nbgrader submit), make sure you are inside a real JupyterHub user server. Running them in a standalone test pod can raise:
KeyError: 'USER'
If ngshare starts with:
sqlite3.OperationalError: unable to open database file
Your storage backend likely doesn’t honor fsGroup.
Fix it by uncommenting the deployment.initContainers block in nbgrader/ngshare-config.yaml and re-install:
helm upgrade ngshare ngshare/ngshare \
--namespace jhub \
-f nbgrader/ngshare-config.yamlThen restart the pod:
kubectl -n jhub delete pod -l app.kubernetes.io/instance=ngshare