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