Skip to content

Multi course multi grader setup using nginx and Shibboleth

Zsolt Szabó edited this page Mar 9, 2024 · 10 revisions

Kick-off

Remarks: Originally, this HowTo describes a composed docker setup of nginx, shibboleth, and jupyterhub (v3.x, with notebook v6.x, nbgrader v0.6) dated in 2023 Jan or Feb.

The comments in such Remarks sections give hints for a new, separated docker setup from nginx (with shibboleth) and docker-gen (which automatically refreshes the configuration of nginx and restarts it whenever a new virtual host was started in a docker container).

Sources:

Pre-requisites:

  • properly registered DNS record for the JupyterHub server (replace string FQDN.YOUR.SERV.ER in the followings, accordingly)
  • available SSL port (443) - tried to use other ports, too, but no luck (Shibboleth requires 443 or 8443, AFAIK)
  • registered Shibboleth SP (if jhub-shibboleth-user-authenticator is going to be used)
  • SSL certificates, e.g. via Let's Encrypt
  • (debian bullseye) linux distro on server with
  • docker.io, docker-compose pkgs.
    (jupyterhub/jupyterhub:3.1.0 image seems to require docker.io v20.10.5, which can be found in bullseye)

Setup nginx / shibboleth / JupyterHub

Remarks: nginx-shibboleth can be installed separately with docker-gen (this is an advanced setup for swarms, though the original can also be used).

The updated jupyterhub and nbgrader setup can be found at the end.

Get docker images:

docker image pull gesiscss/nginx-shibboleth
docker image pull jupyterhub/jupyterhub

Create directories for docker-compose:

mkdir -p /var/lib/docker-jhub/{nginx,jupyterhub}
cd /var/lib/docker-jhub

Nginx and Shibboleth configuration

Patched nginx/nginx_shibboleth.conf:

map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
}

server {
    listen 443 ssl; # Shibboleth requires 443 or 8443
    server_name FQDN.YOUR.SERV.ER

    ssl_certificate     /etc/letsencrypt/live/FQDN.YOUR.SERV.ER/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/FQDN.YOUR.SERV.ER/privkey.pem;

    #FastCGI authorizer for Auth Request module
    location = /shibauthorizer {
        internal;
        include fastcgi_params;
        fastcgi_pass unix:/var/run/shibboleth/shibauthorizer.sock;
    }

    #FastCGI responder
    location /Shibboleth.sso {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/shibboleth/shibresponder.sock;
    }

    #Resources for the Shibboleth error pages. This can be customised.
    location /shibboleth-sp {
        alias /usr/share/shibboleth/;
    }

    underscores_in_headers on;

    #A secured location, but only a specific sub-path causes Shibboleth
    #authentication.
    location / {
        proxy_pass https://jhub:8000;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection $connection_upgrade;
	proxy_set_header Host $host; # or $http_host if port is to be proxied?
	proxy_set_header Origin "";  # ???

        location = /hub/login { # edit shibboleth2.xml accordingly to this...
            include shib_clear_headers;
            #Add your attributes here. They get introduced as headers
            #by the FastCGI authorizer so we must prevent spoofing.
            more_clear_input_headers 'displayName' 'email' 'persistent-id';
            # check further attributes and their capitalization which
            # can be extracted from the header obtained from your IDp:
            #more_clear_input_headers
            #    'Mail' 'Givenname' 'Eppn' 'Displayname' 'Affiliation' 'Sn' 'Ou'
            #    'Persistent-Id' 'Shib-Session-Id' 'Auth_type' 'Remote_user';

             shib_request /shibauthorizer;
             shib_request_use_headers on;
             proxy_pass https://jhub:8000;
         }
     }
}

Advanced setup:

docker image pull jwilder/docker-gen
mkdir -p /var/lib/docker-nginx/{conf.d,vhost.d}
curl -s https://github.com/nginx-proxy/nginx-proxy/commits/main/nginx.tmpl >/var/lib/docker-nginx/nginx.tmpl

split nginx_shibboleth.conf:

  • rows from location = /shibauthorizer to underscore_in_headers: vhost.d/FQDN.YOUR.SERV.ER
  • rows of group location = /hub/login: vhost.d/FQDN.YOUR.SERV.ER_local

link certificates in /etc/letsencrypt folder (assuming that only one certificate pair is requested for every CNAME or virtual host):

ln -s live/FQDN.YOUR.SERV.ER/fullchain.pem default.crt
ln -s live/FQDN.YOUR.SERV.ER/privkey.pem default.key

continue with the modification of docker-compose.yaml below...

Populate nginx/shibboleth_conf (it is another story) and edit shibboleth2.xml, attribute-map.xml, etc.

Get and patch docker-compose.yaml

patch docker-compose.yaml <<EOF
--- example-docker-compose.yaml	2022-01-20 22:30:25.795316503 +0100
+++ docker-compose.yaml	2022-01-20 22:30:40.019305291 +0100
@@ -3,8 +3,8 @@
 services:
 
     jhub:
-        image: gesiscss/jupyterhub-jsa:v0.8.1
-        deploy:
+        image: jupyterhub/jupyterhub
+        deploy: # considered only with `docker stack deploy`
           replicas: 1
           restart_policy:
               condition: on-failure
@@ -12,19 +12,21 @@
               limits:
                   cpus: "0.2"
                   memory: 512M
-        volumes:
-            - path/to/jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py
         restart: always
+        volumes:
+            - /var/lib/docker-jhub/jupyterhub:/srv/jupyterhub
+            - /etc/letsencrypt:/etc/letsencrypt
+            - JHUB_HOME:/home
         expose:
             - "8000"
         networks:
-            - webnet
+            - jhub-net
 
-    nginx-shibboleth:
+    nginx-shib:
         depends_on:
             - jhub
         image: gesiscss/nginx-shibboleth:v0.2.2
-        deploy:
+        deploy: # considered only with `docker stack deploy`
           replicas: 1
           restart_policy:
               condition: on-failure
@@ -34,33 +36,18 @@
                   memory: 512M
         volumes:
             # add Shibboleth configuration
-            - path/to/shibboleth:/etc/shibboleth
+            - /var/lib/docker-nginx/shibboleth_conf:/etc/shibboleth
             # add nginx configuration
-            - path/to/shibboleth_nginx.conf:/etc/nginx/conf.d/shibboleth_nginx.conf
-            # below here depends on your project requirements
-            # if you want to use shibboleth eds
-            - path/to/embedded_discovery_service:/home/shibboleth/embedded_discovery_service
+            - /var/lib/docker-nginx/nginx_shibboleth.conf:/etc/nginx/conf.d/nginx_shibboleth.conf
             # if you want to use letsencrypt to enable https
             - /etc/letsencrypt:/etc/letsencrypt
             - /etc/ssl:/etc/ssl
         ports:
-            - "80:80"
             - "443:443"
         restart: always
         command: /usr/bin/supervisord --nodaemon --configuration /etc/supervisor/supervisord.conf
         networks:
-            - webnet
-    visualizer:
-        image: dockersamples/visualizer:stable
-        ports:
-          - "8080:8080"
-        volumes:
-          - "/var/run/docker.sock:/var/run/docker.sock"
-        deploy:
-          placement:
-            constraints: [node.role == manager]
-        networks:
-          - webnet
+            - jhub-net
 networks:
-  webnet:
+    jhub-net:
EOF

Advanced setup (contd.):

Add VIRTUAL_* parameters to jhub service definition:

environment:
  - VIRTUAL_HOST=FQDN.YOUR.SERV.ER
  - VIRTUAL_PORT=8000
  - VIRTUAL_PROTO=https
  - CERT_NAME=default
  - DEBUG=true # if something went wrong

"Patch" nginx-shib service definition:

          image: gesiscss/nginx-shibboleth:v0.2.2
+         container_name: nginx-shib
[...]
-            - /var/lib/docker-nginx/nginx_shibboleth.conf:/etc/nginx/conf.d/nginx_shibboleth.conf
+            - /var/lib/docker-nginx/conf.d:/etc/nginx/conf.d
             # if you want to use letsencrypt to enable https
-            - /etc/letsencrypt:/etc/letsencrypt
+            - /etc/letsencrypt:/etc/nginx/certs:ro

Add nginx-gen service:

    nginx-gen:
      image: jwilder/docker-gen
      command: -notify-sighup nginx-shib -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
      container_name: nginx-gen
      restart: unless-stopped
      volumes:
        - nginx-conf:/etc/nginx/conf.d # :rw !!!
        - nginx-vhost:/etc/nginx/vhost.d
#        - /srv/www/nginx-proxy/html:/usr/share/nginx/html
        - /etc/letsencrypt:/etc/nginx/certs:ro
        - /var/run/docker.sock:/tmp/docker.sock:ro
        - /var/lib/docker-nginx/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
      networks:
        - jhub-net

Start containers:

docker-compose up -d
docker-compose logs -f --tail 50
# create /var/log:
#docker-compose logs -f jhub > /var/log/jhub.log &
# and create /etc/logrotate.d/jhub.conf ...

Upgrade packages

Execute commands in jupyterhub container:

docker-compose exec jhub bash
apt update
apt upgrade
apt install mc nano less # etc.
chmod o-rx /home # it is worth denying access to list /home
exit

Basic JupyterHub configuration

Generate basic jupyterhub_config.py:

docker-compose exec jhub bash
jupyterhub --generate-config # though this contains only commented lines...
# (change or) add the following lines to it:
cat >>jupyterhub_config.py <<EOF
c.JupyterHub.ssl_cert = '/etc/letsencrypt/live/FQDN.YOUR.SERV.ER/fullchain.pem'
c.JupyterHub.ssl_key = '/etc/letsencrypt/live/FQDN.YOUR.SERV.ER/privkey.pem'

c.Authenticator.admin_users = {'UID_FROM_SHIBBOLETH_TO_BE_ADMIN'}

c.JupyterHub.authenticator_class = 'jhub_shibboleth_user_authenticator.shibboleth_user_auth.ShibbolethUserAuthenticator
# use a different cookie entry as the user name:
c.Authenticator.header_name = 'ATTRIBUTE_FROM_SHIBBOLETH'

# put some extra values in the auth_state for the spawner
# don't forget to activate c.Authenticator.enable_auth_state = True (??? IS THIS WORKING ???)
c.Authenticator.auth_state_header_names = [
     'Sn', 'Persistent-Id', 'Ou', 'Mail', 'Givenname', 'Eppn', 'Displayname',
     'Affiliation', 'Shib-Session-Id', 'Auth_type', 'Remote_user'
]
EOF
adduser -q --gecos "" --disabled-password UID_FROM_SHIBBOLETH_TO_BE_ADMIN
pip install jhub-shibboleth-user-authenticator==0.1.6
exit

If you want to test your setup and login to your freshly configure JupyterHub server, you need to install notebook (which is otherwise installed automatically with nbgrader):

docker-compose exec jhub pip install notebook

Now, restart jhub container and you can try to login with Shibboleth authentication. (If not using Shibboleth, the last 10 line can be dropped, and you can use the default PAM authenticator, or an OAUTH2 method...)

docker-compose restart jhub

Nbgrader setup

Install nbgrader in editable mode (if the source needs to be patched):

docker-compose exec jhub bash
apt install git
cd /usr/local
pip install -e git+https://github.com/jupyter/nbgrader#egg=nbgrader
jupyter-nbextension install nbgrader --system --py --overwrite
jupyter-nbextension enable nbgrader --system --py
jupyter-serverextension enable nbgrader --system --py

Disable formgrader and course_list modules for regular users (students):

for m in formgrader course_list; do
    jupyter-nbextension disable $m/main --system --section=tree
    jupyter-serverextension disable nbgrader.server_extensions.$m --system
done
# to disable nbgrader cell-toolbar for regular users (aka. students):
jupyter-nbextension disable create_assignment/main --system
#
mkdir -p /srv/nbgrader/exchange
chmod a+rw /srv/nbgrader/exchange
ln -s /srv/jupyterhub /etc/jupyter

Create global nbgrader_config.py:

cat <<EOF >/etc/jupyter/nbgrader_config.py
from nbgrader.auth import JupyterHubAuthPlugin
c = get_config()
c.Exchange.path_includes_course = True
c.Authenticator.plugin_class = JupyterHubAuthPlugin
EOF
exit # from docker container

Create and setup courses

Add service with necessary role privileges to jupyterhub_config.py:

jhub_api_token = 'output of `openssl rand -hex 32`'
c.JupyterHub.services = [
    { 'name': 'formgrader-service',
      'api_token': jhub_api_token
    },
]
# spawner needs JHUB_API_TOKEN for querying students' groups
# "required to run the exchange features of nbgrader"
c.Spawner.environment = {
    'JHUB_API_TOKEN': jhub_api_token,
}
c.JupyterHub.load_roles = [
    { # this is for auth API
        'name': 'formgrade-role',
        'scopes': [ 'read:users:groups', 'list:services', 'groups', 'admin:users' ],
        'services': [ 'formgrader-service' ]
    }
]
c.JupyterHub.load_groups = {
# populate with formgrade-* and nbgrader-* groups (see make courses below)
}

Remark: 'admin:users' in scopes is needed only if command line API access (for administration of users and groups) uses the same API token. This can be a security issue, hence, creating a separate service / role / API_token is recommended for this purpose.

Add this to jupyterhub/Makefile:

grader-%:
	adduser -q --gecos "" --disabled-password $@
	su - $@ -c "chmod go-rwx ."
	@c_id=`echo $* |tr [a-z] [A-Z]`; \
	cdir="$$(grep $@ /etc/passwd |cut -f6 -d:)/OPTIONAL_PREFIX-$$c_id"; \
	nbconf="c=get_config\(\)\|\
c.CourseDirectory.root = \'$$cdir\'\|\
c.CourseDirectory.course_id = \'$$c_id\'";\
	su - $@ -c "if ! [ -e .jupyter ]; then mkdir .jupyter; fi; \
echo $$nbconf |tr '|' '\n' >.jupyter/nbgrader_config.py; \
nbgrader quickstart $$cdir && rm -rf $$cdir/source/ps1; \
sed -ri '/course_id/s/^/#/' $$cdir/nbgrader_config.py"
	su - $@ -c "jupyter-nbextension enable formgrader/main --user --section=tree"
	su - $@ -c "jupyter-serverextension enable nbgrader.server_extensions.formgrader --user"
	su - $@ -c "jupyter-nbextension enable create_assignment/main --user"
	su - $@ -c "jupyter-nbextension disable assignment_list/main --user --section=tree"
	su - $@ -c "jupyter-serverextension disable nbgrader.server_extensions.assignment_list --user"

gr-%: grader-%
	@c_id=`echo $* |tr [a-z] [A-Z]`; \
	echo -e "Add this snippet to the appropriate sections in jupyterhub_config.py (replace UNIQUE_PORT!):\\n\
--- 8< ---\\n\
#c.JupyterHub.service = [\\n\
    { 'name': '$$c_id',\\n\
      'url': 'http://127.0.0.1:UNIQUE_PORT',\\n\
      'command': [\\n\
        'jupyterhub-singleuser',\\n\
        '--group=formgrade-$$c_id',\\n\
        '--debug'\\n\
      ],\\n\
      'environment': { 'JHUB_API_TOKEN': jhub_api_token },\\n\
      'user': '$<',\\n\
      'cwd': '/home/$<',\\n\
      #'api_token': '$$(openssl rand -hex 16)'\\n\
    },\\n\
#]\\n\
#c.JupyterHub.load_groups = {\\n\
    'formgrade-$$c_id': [ '$<' ],\\n\
    'nbgrader-$$c_id': [],\\n\
#}\\n\
#c.JupyterHub.load_roles = [\\n\
    { 'name':   'role-$*',\\n\
      'scopes': [ 'access:services!service=$$c_id' ],\\n\
      'groups': [ 'formgrade-$$c_id' ]\\n\
    },\\n\
#]\\n\
--- >8 ---"

Now, execute the command

docker-compose exec jhub make gr-NEW_COURSE_ID

and patch jupyterhub_config.py as suggested.

Create and setup tutors (instructors)

Assuming a TAB separated tutor_ids.txt file with first column as id and the second one as the list of COURSE_IDs which the tutor of the given id should be member of, run this script or add similar lines to a Makefile:

docker-compose exec jhub bash
for id in `cut -f1 tutor_ids.txt |tr [A-Z] [a-z]`; do
    adduser -q --gecos "" --firstuid 2000 --gid 2000 --disabled-password $id
    su - $id -c "chmod go-rwx ."
    su - $id -c "jupyter-nbextension enable course_list/main --user --section=tree"
    su - $id -c "jupyter-serverextension enable nbgrader.server_extensions.course_list --user"
    # the following three lines are optional:
    su - $id -c "jupyter-nbextension enable create_assignment/main --user"
    su - $id -c "jupyter-nbextension disable assignment_list/main --user --section=tree"
    su - $id -c "jupyter-serverextension disable nbgrader.server_extensions.assignment_list --user"
done
python3 api_request.py users add `cut -f1 tutor_ids.txt`
# see api_request.py below...

After generating a reverse map from tutor_ids.txt as course_graders.txt with first column as COURSE_ID and second column as the list of graders of that course, the courses can be populated with their graders:

cat course_graders.txt |while read COURSE_ID GRADER_LIST; do
    python3 api_request.py groups add formgrade-${COURSE_ID} ${GRADER_LIST}`
done
exit

The jupyterhub/api_request.py for user / group management from command line:

# this script may require some more detailed use cases...
import sys

def query( q, m="GET", d="" ):
  api_url = 'http://127.0.0.1:8081/hub/api'
  url = api_url + q
  h = { 'accept': 'application/json',
	'Authorization': 'token YOUR_jhub_api_token_FROM_jupyterhub_config.py'
  }
  print( url + " %s:" % m )
  if m == "GET":
     return requests.get( url, headers=h )
  elif m == "POST":
     print(d)
     return requests.post( url, headers=h, json=d )
  elif m == "DELETE":
     print(d)
     return requests.delete( url, headers=h, json=d )

input= sys.argv
argc = len(input)

if argc < 2:
   print( "Usage: " + input[0] + " {users|groups} [add|remove] \$ug_list" )

ug = input[1] # users or groups
if argc == 2: # just listing the users / groups existing in jupyterhub
   q = query( '/'+ug )
   gu= 'groups'
   if ug == gu: gu = 'users'
   #print(q.json()) # just for testing purpose
   for e in q.json():
       print( "%s: " % e.get('name'), end="" )
       print( e.get(gu) )
elif input[2] == "add" or input[2] == "remove":
   input.pop(0)
   ug = input.pop(0)
   gu= 'usernames'
   if ug == 'groups': gu = 'users'
   m = input.pop(0)
   if m == "add": m = "POST"
   else: m = "DELETE" # remove
   if ug == 'groups':
      name = input.pop(0)
      # e.g. /groups/formgrade-${COURSE_ID}/users POST { 'users': ${TUTOR_IDS} }
      q = query( '/'+ug+'/'+name+'/'+gu, m, { gu: input } )
   elif ug == 'users' and m == 'POST':
      # e.g. /users POST { 'usernames': ${TUTOR_IDS} }
      q = query( '/'+ug, m, { gu: input } )
   else:
      print("Unknown combination!")
   print( q.json() )
else:
   print("Unknown method!")

Create and setup students

Assuming a students.csv in the form and with appropriate header (only the first column is necessary) as follows

id last_name first_name email lms_user_id

Create students' jupyterhub accounts and register theirs IDs in jupyterhub database:

docker-compose exec jhub bash
tail -n +2 students.csv |cut -f1 -d, |tr -d '"' |tr [A-Z] [a-z] |while read id; do
    adduser -q --gecos "" --firstuid 3000 --gid 3000 --disabled-password $id
    su - $id -c "chmod go-rwx ."
    echo -n "."
done
python3 api_request.py users add `tail -n +2 students.csv |cut -f1 -d, |tr -d '"'`

Now, if students.csv contains students of a course of certain COURSE_ID only, they can be imported:

su - grader-COURSE_ID -c "nbgrader db student import students.csv"
exit

Setup jupyterhub-idle-culler service

Source: https://github.com/jupyterhub/jupyterhub-idle-culler

Install:

docker-compose exec jhub pip install jupyterhub-idle-culler

Modify jupyterhub_config.py:

#c.JupyterHub.services = [
#insert the following lines in this section (services):
    { 'name': 'culler-service',
      'command': [
            'jupyterhub-idle-culler', '--timeout=21600', '--url=http://127.0.0.1:8081/hub/api'
      ],
      'api_token': 'output of `openssl rand -hex 16`, e.g.' # if wanted to use its role through API requests
    },
#...
#]
#c.JupyterHub.load_roles = [
#insert the following lines in this section (load_roles):
    {   'name':   'idle-culler-role',
        'scopes': [
            'list:users', 'read:users:activity',
            'read:servers', 'delete:servers',
            'read:users:groups', 'admin:users' # needed for api_request.py, e.g.
        ],
        'services': [ 'culler-service' ]
    },
    { # now, scopes of formgrade-role can be narrowed:
        'name':     'formgrade-role',
        'scopes':   [ 'read:users:groups', 'list:services', 'groups' ],
        'services': [ 'formgrader-service' ]
    },
#]

Updates (2024-03-09)

JupyterHub setup

Let us create an updated image for ourselves with Dockerfile:

FROM jupyterhub/jupyterhub:4.0.2

# user dirs and submissions are assumed to be on the mapped volume of /home:
ARG EXCHANGE=/home/Exchange
ARG NBGRADER_SRC=/usr/local/lib/python3.10/dist-packages/nbgrader
ARG NBGRADER_PIP=nbgrader
ARG JLAB_LTX_PIP="git+https://github.com/jupyterlab/jupyterlab-latex#egg=jupyterlab-latex"
ARG TIMEZONE=Europe/Budapest

#RUN echo $TIMEZONE > /etc/timezone && apt-get install -y tzdata
RUN apt-get update && apt-get -y upgrade &&\
    apt-get -y install mc nano less lynx git patch make recode rsync jq &&\
    apt-get -y install cmake pkg-install

#RUN python3 -m pip install --upgrade pip
RUN pip install jhub-shibboleth-user-authenticator \
    jupyterhub-idle-culler $NBGRADER_PIP
# default setup is for students, who need only assignment-list module:
RUN jupyter-labextension disable --level=system nbgrader:course-list;\
    jupyter-server extension disable --system nbgrader.server_extensions.course_list;\
    jupyter-labextension disable --level=system nbgrader:formgrader;\
    jupyter-server extension disable --system nbgrader.server_extensions.formgrader;\
    jupyter-labextension disable --level=system nbgrader:create-assignment
RUN mkdir /srv/nbgrader;\
    if ! [ -e $EXCHANGE ]; then mkdir -m 777 $EXCHANGE; fi;\
    ln -s $EXCHANGE /srv/nbgrader;\
    ln -s /srv/jupyterhub /etc/jupyter

# jupyterlab-git 0.44.0 does not work with JupyterLab 4.x:
RUN pip install --pre "jupyterlab-git==0.50.0a1"
RUN npm install --global yarn
RUN curl -sL https://deb.nodesource.com/setup_18.x |bash - ;\
    apt-get purge -y --auto-remove nodejs && apt-get install -y nodejs
# jupyterlab-latex 4.0.0:
RUN pip install $JLAB_LTX_PIP && rm -rf /root/{.npm,.yarn} && ls -lA /root

#RUN pip install jupyter_c_kernel && python3 \
#    /usr/local/lib/python3*/dist-packages/jupyter_c_kernel/install_c_kernel --user
#RUN apt-get -y install gcc

# these packages can be installed in the image, though, it increases its size quite much:
#RUN apt-get -y install scilab aptitude pandoc texlive-xetex texlive-pstricks \
#       texlive-lang-european texlive-science octave-control octave-image \
#       octave-ga octave-linear-algebra octave-quaternion octave-specfun \
#       netpbm poppler-utils python3-csvkit
RUN pip install scilab_kernel octave-kernel gnuplot_kernel &&\
    cd /usr/local; mkdir share/jupyter/kernels/gnuplot; \
    cp -a lib/python3*/dist-packages/gnuplot_kernel/images/logo-* \
          share/jupyter/kernels/gnuplot/; \
    cd share/jupyter/kernels; ln octave/images/logo-* octave/;\
    cat octave/kernel.json |sed -r 's/[oO]ctave/gnuplot/' > gnuplot/kernel.json

RUN pip install matplotlib scipy sympy jupyterlab-rise ipympl
# if classic RISE would be preferred:
#RUN pip install nbclassic rise

Now, run: docker build -t my_jhub:4.0.2_XXXX .

The docker-compose.yaml file can be something like this:

version: '3'

services:
    jhub:
        image: my_jhub:4.0.2_XXXX
        container_name: jhub
        volumes:
            - /etc/jupyterhub:/srv/jupyterhub
# only needed if VIRTUAL_PROTO=https, which also needs 'proxy_pass https:...':
#            - _letsencrypt_:/etc/letsencrypt
            - /home/Docker/jhub:/home
        environment:
            - VIRTUAL_HOST=jupyterhub.MY-DOMA.IN
            - VIRTUAL_PORT=8000
# this requires 'proxy_pass http://jupyterhub.MY-DOMA.IN' in nginx-proxy.conf
# and no ssl_{cert,key} in jupyterhub_config.py, no letsencrypt in jhub container:
            - VIRTUAL_PROTO=http
            - CERT_NAME=default
            - DEBUG=true
        expose:
            - "8000"
#        restart: unless-stopped
#???     command: ./00start.sh
        networks:
            - nginx
networks:
# from nginx / docker-gen !!!
  nginx:
    external:
      name: nginx_default

Now, it can be started as: docker-compose start jhub

Nbgrader setup

So, we have:

pip list |egrep '(^jupyter..b |nbg|noteb)|idle'
jupyterhub                         4.0.2
jupyterhub-idle-culler             1.2.1
jupyterlab                         ==4.0.12
nbgrader                           0.9.1
notebook                           ==7.0.8
notebook_shim                      0.2.4

Note for freezed versions: https://github.com/jupyter/nbgrader/issues/1866

Modify jupyterhub_config.py for multi-grader / multiple-class scenario:

c.JupyterHub.services = [
    { 'name': 'culler-service',
      'command': [
          'jupyterhub-idle-culler', '--timeout=21600', '--url=http://127.0.0.1:8081/hub/api'
      ],
      'api_token': 'VERY_SECRET_TOKEN'
    },
    { 'name': 'MECH', # for class MECH
      'url':  'http://127.0.0.1:9999',
      'command': [ 'jupyterhub-singleuser',
        '--group=formgrade-MECH', '--debug' ],
      'user': 'grader-mech',
      'cwd':  '/home/grader-mech',
    }
]
c.JupyterHub.load_roles = [
    { # needed for course_list to work, see: https://jupyterhub.readthedocs.io/en/stable/rbac/roles.html
        'name': 'server', 'scopes': [ 'inherit' ]
    },
    {   'name':   'idle-culler-role',
        'scopes': [
#            'list:users', 'read:users:activity', # if no admin:users below, see: https://github.com/jupyterhub/jupyterhub-idle-culler
            'read:servers', 'delete:servers', 'admin:users',
#            'admin:groups', # if wanted CLI admin through REST API
        ],
        'services': [ 'culler-service' ]
    },
    # access courses by its tutors only:
    {   'name':   'role-mech', # must be LOWERCASE!
        'scopes': [ 'access:services!service=MECH',
                    # needed to access course_list as NON-ADMIN (see https://github.com/jupyter/nbgrader/issues/1831):
                    'list:services', 'read:services!service=MECH',
                    # needed for grader to administrate student (add, remove, list to group)
                    'read:users', 'groups!group=nbgrader-MECH' ],
        'groups': [ 'formgrade-MECH' ],
        'services': [ 'MECH' ] # needed to be able to use formgrader
    }
]
c.JupyterHub.load_groups = {
# grader groups - add tutors, too, on JupyterHub admin page or via cmdline
    'formgrade-MECH': { 'users': [ 'grader-mech' ] },
# student groups - add students on (formgrader/admin?) page (or via cmdline ?)
    'nbgrader-MECH': { 'users': [] }
}

Check global nbgrader_config.py (see above).

Further setups for the example grader-mech user:

adduser -q --gecos "" --disabled-password grader-mech
su - grader-mech
![ -e .jupyter ] && mkdir .jupyter
![ -d .jupyter ] && echo ".jupyter is not directory" && exit 1
echo <<EOF >.jupyter/nbgrader_config.py 
c = get_config()
c.CourseDirectory.course_id = 'MECH'
c.CourseDirectory.root = '/home/grader-mech/Course-MECH'
c.IncludeHeaderFooter.header = "source/header.ipynb"
EOF
jupyter-server extension disable --user nbgrader.server_extensions.assignment_list
jupyter-server extension enable --user nbgrader.server_extensions.formgrader
jupyter-labextension disable --level=user nbgrader:assignment-list
jupyter-labextension enable --level=user nbgrader:formgrader
jupyter-labextension enable --level=user nbgrader:create-assignment
# or instead of the last 3 commands execute the next ones:
#cp /usr/local/etc/jupyter/labconfing .jupyter/ &&
#sed -ri '3s/course/assignment/;4,5s/true/false/' .jupyter/labconfig/page_config.json

Setup for an instructor named mech-tutor:

su - mech-tutor
jupyter-server extension enable --user nbgrader.server_extensions.course_list
jupyter-labextension enable --level=user nbgrader:course-list

(mech-tutor is to be added to group formgrade-MECH on admin page!)

Clone this wiki locally