How to entry user-selectable page with Dockerlized Jupyterhub

I am currently building JupyterHub on single machine using Docker, and I want users to access a page where they can select the Docker image they want upon logging in, like the below page.

But whenever I log in using any account, the system always redirects me to the JupyterLab interface, without presenting an option to choose the desired image.

I have try navigate to localhost:8000/hub/spawn, but always get 302 https method and redirect to the main page of JupyterLab.
302 GET /hub/spawn → /user/admin/ (admin@::ffff:172.20.0.1)

Have I overlooked something in the configuration?

Below are my configuration file and logs for reference.

jupyterhub_config.py

import os

c = get_config()  # noqa

c.JupyrerHub.spawner_class = 'dockerspawner.DockerSpawner'
c.DockerSpawner.network_name = os.environ['DOCKER_NETWORK_NAME']


c.DockerSpawner.allowed_images = {
    "scipy":"quay.io/jupyter/base-notebook:2023-12-04",
    "datascience":"quay.io/jupyter/datascience-notebook:2023-12-08",
}

c.DockerSpawner.use_internal_ip = True
c.DockerSpawner.extra_host_config = {'network_mode': 'jupyter_hub'}
c.Spawner.environment = {'GRANT_SUDO': 'yes'}

from jupyter_client.localinterfaces import public_ips
from subprocess import check_call

c.JupyterHub.hub_ip = '0.0.0.0'
c.JupyterHub.admin_access = True
c.Authenticator.admin_users = {'admin'}
c.Authenticator.delete_invalid_users = True


c.JupyterHub.authenticator_class = 'dummy'
c.LocalAuthenticator.create_system_users = True


def create_dir_hook(spawner):
    """ Create directory """
    username = spawner.user.name  # get the username
    home_path = os.path.join('/persist/', username)
    try:
        check_call(['useradd', '-ms', '/bin/bash', username])
    except Exception as e:
        print(f'{e}')
    if not os.path.exists(home_path):
        os.mkdir(home_path)
        os.chown(home_path, 1000, 1000)  # Same UID/GID as in local machine


c.Spawner.pre_spawn_hook = create_dir_hook
notebook_dir = os.environ.get('DOCKER_NOTEBOOK_DIR') or '/home/jovyan/work'
c.DockerSpawner.notebook_dir = notebook_dir
c.DockerSpawner.volumes = {'/Users/apple/Desktop/jupyterhub/{username}': '/home/jovyan/work'}

c.JupyterHub.cookie_secret_file = '/persist/jupyterhub_cookie_secret'
c.JupyterHub.db_url = '/persist/jupyterhub.sqlite'

Dockerfile

FROM jupyterhub/jupyterhub:4.0
COPY jupyterhub_config.py .
RUN pip install \
    dockerspawner \
    jupyterhub-client \
    jupyterhub-firstuseauthenticator \
    escapism

ARG NB_USER="jovyan"
ARG NB_UID="1000"
ARG NB_GID="100"

RUN pip install jupyterlab --upgrade

Jupyterlab/Dockerfile

FROM quay.io/jupyter/base-notebook:2023-12-04
FROM quay.io/jupyter/datascience-notebook:2023-12-08

RUN pip install jupyterlab --upgrade

docker-compose.yaml

version: '3'
services:
  # Configuration for Proxy+Hub
  jupyterhub:
    build: . # Build the container from this folder.
    container_name: jupyterhub
    # image: jupyterhub/jupyterhub # Specify the image to start the container from. container_name: jupyterhub # Specify the custom container name.
    volumes: # Mount host paths or named volumes.
    # Configuration for the single-user servers
      - /var/run/docker.sock:/var/run/docker.sock # Mount the Docker socket so that containers launched by the Hub can be run with the same privileges as the Hub itself.
      - .:/persist # Hub data persistence 
      - jupyterhub_data:/srv/jupyterhub # Hub data persistence
    environment: # Env variables passed to the Hub process.
      DOCKER_NETWORK_NAME: jupyter_hub
    restart: unless-stopped # The restart policy. 
    ports:
      - "8000:8000"

  jupyterlab:
    build: ./jupyterlab
    #image: jupyter/base-notebook
    command: echo

volumes:
  jupyterhub_data:

The error logs of Docker

jupyterhub               | [I 2023-12-15 09:27:27.882 JupyterHub app:2859] Running JupyterHub version 4.0.2
jupyterhub               | [I 2023-12-15 09:27:27.882 JupyterHub app:2889] Using Authenticator: jupyterhub.auth.DummyAuthenticator-4.0.2
jupyterhub               | [I 2023-12-15 09:27:27.882 JupyterHub app:2889] Using Spawner: jupyterhub.spawner.LocalProcessSpawner-4.0.2
jupyterhub               | [I 2023-12-15 09:27:27.883 JupyterHub app:2889] Using Proxy: jupyterhub.proxy.ConfigurableHTTPProxy-4.0.2
jupyterhub               | [I 2023-12-15 09:27:27.913 JupyterHub app:1664] Loading cookie_secret from /persist/jupyterhub_cookie_secret
jupyterhub               | [I 2023-12-15 09:27:28.061 JupyterHub proxy:556] Generating new CONFIGPROXY_AUTH_TOKEN
jupyterhub               | [I 2023-12-15 09:27:28.092 JupyterHub app:1984] Not using allowed_users. Any authenticated user will be allowed.
jupyterhub               | [I 2023-12-15 09:27:28.174 JupyterHub app:2928] Initialized 0 spawners in 0.005 seconds
jupyterhub               | [I 2023-12-15 09:27:28.184 JupyterHub metrics:278] Found 1 active users in the last ActiveUserPeriods.twenty_four_hours
jupyterhub               | [I 2023-12-15 09:27:28.186 JupyterHub metrics:278] Found 1 active users in the last ActiveUserPeriods.seven_days
jupyterhub               | [I 2023-12-15 09:27:28.188 JupyterHub metrics:278] Found 1 active users in the last ActiveUserPeriods.thirty_days
jupyterhub               | [W 2023-12-15 09:27:28.189 JupyterHub proxy:746] Running JupyterHub without SSL.  I hope there is SSL termination happening somewhere else...
jupyterhub               | [I 2023-12-15 09:27:28.189 JupyterHub proxy:750] Starting proxy @ http://:8000
jupyterhub               | 09:27:28.579 [ConfigProxy] info: Proxying http://*:8000 to (no default)
jupyterhub               | 09:27:28.588 [ConfigProxy] info: Proxy API at http://127.0.0.1:8001/api/routes
jupyterhub               | [I 2023-12-15 09:27:28.656 JupyterHub app:3178] Hub API listening on http://0.0.0.0:8081/hub/
jupyterhub               | [I 2023-12-15 09:27:28.656 JupyterHub app:3180] Private Hub API connect url http://04ea636e20c2:8081/hub/
jupyterhub               | [I 2023-12-15 09:27:28.657 JupyterHub app:3198] Adding external service admin
jupyterhub               | 09:27:28.656 [ConfigProxy] info: 200 GET /api/routes 
jupyterhub               | 09:27:28.659 [ConfigProxy] info: 200 GET /api/routes 
jupyterhub               | [I 2023-12-15 09:27:28.659 JupyterHub proxy:477] Adding route for Hub: / => http://04ea636e20c2:8081
jupyterhub               | 09:27:28.663 [ConfigProxy] info: Adding route / -> http://04ea636e20c2:8081
jupyterhub               | 09:27:28.664 [ConfigProxy] info: Route added / -> http://04ea636e20c2:8081
jupyterhub               | 09:27:28.666 [ConfigProxy] info: 201 POST /api/routes/ 
jupyterhub               | [I 2023-12-15 09:27:28.666 JupyterHub app:3245] JupyterHub is now running at http://:8000
jupyterhub               | [I 2023-12-15 09:27:43.020 JupyterHub log:191] 302 GET / -> /hub/ (@::ffff:172.20.0.1) 3.63ms
jupyterhub               | [I 2023-12-15 09:27:43.046 JupyterHub log:191] 302 GET /hub/ -> /hub/login?next=%2Fhub%2F (@::ffff:172.20.0.1) 1.00ms
jupyterhub               | [I 2023-12-15 09:27:43.126 JupyterHub log:191] 200 GET /hub/login?next=%2Fhub%2F (@::ffff:172.20.0.1) 58.20ms
jupyterhub               | [I 2023-12-15 09:27:51.466 JupyterHub roles:238] Adding role user for User: test
jupyterhub               | [I 2023-12-15 09:27:51.526 JupyterHub base:837] User logged in: test
jupyterhub               | [I 2023-12-15 09:27:51.527 JupyterHub log:191] 302 POST /hub/login?next=%2Fhub%2F -> /hub/ (test@::ffff:172.20.0.1) 83.49ms
jupyterhub               | [I 2023-12-15 09:27:51.588 JupyterHub log:191] 302 GET /hub/ -> /hub/spawn (test@::ffff:172.20.0.1) 40.46ms
jupyterhub               | [I 2023-12-15 09:27:51.682 JupyterHub provider:659] Creating oauth client jupyterhub-user-test
jupyterhub               | [I 2023-12-15 09:27:51.821 JupyterHub spawner:1689] Spawning jupyterhub-singleuser
jupyterhub               | [I 2023-12-15 09:27:52.610 JupyterHub log:191] 302 GET /hub/spawn -> /hub/spawn-pending/test (test@::ffff:172.20.0.1) 1005.78ms
jupyterhub               | [I 2023-12-15 09:27:52.628 JupyterHub pages:398] test is pending spawn
jupyterhub               | [I 2023-12-15 09:27:52.633 JupyterHub log:191] 200 GET /hub/spawn-pending/test (test@::ffff:172.20.0.1) 9.34ms
jupyterhub               | [W 2023-12-15 09:27:54.581 ServerApp] A `_jupyter_server_extension_points` function was not found in jupyter_lsp. Instead, a `_jupyter_server_extension_paths` function was found and will be used for now. This function name will be deprecated in future releases of Jupyter Server.
jupyterhub               | [I 2023-12-15 09:27:54.772 ServerApp] Extension package jupyterlab took 0.1802s to import
jupyterhub               | [W 2023-12-15 09:27:55.487 ServerApp] A `_jupyter_server_extension_points` function was not found in notebook_shim. Instead, a `_jupyter_server_extension_paths` function was found and will be used for now. This function name will be deprecated in future releases of Jupyter Server.
jupyterhub               | [I 2023-12-15 09:27:55.488 ServerApp] jupyter_lsp | extension was successfully linked.
jupyterhub               | [I 2023-12-15 09:27:55.501 ServerApp] jupyter_server_terminals | extension was successfully linked.
jupyterhub               | [I 2023-12-15 09:27:55.502 JupyterHubSingleUser] Starting jupyterhub single-user server extension version 4.0.2
jupyterhub               | [I 2023-12-15 09:27:55.502 JupyterHubSingleUser] Using default url from server extension lab: /lab
jupyterhub               | [I 2023-12-15 09:27:55.507 ServerApp] jupyterhub | extension was successfully linked.
jupyterhub               | [W 2023-12-15 09:27:55.509 LabApp] 'extra_template_paths' was found in both NotebookApp and ServerApp. This is likely a recent change. This config will only be set in NotebookApp. Please check if you should also config these traits in ServerApp for your purpose.
jupyterhub               | [I 2023-12-15 09:27:55.514 ServerApp] jupyterlab | extension was successfully linked.
jupyterhub               | [I 2023-12-15 09:27:55.515 ServerApp] Writing Jupyter server cookie secret to /home/test/.local/share/jupyter/runtime/jupyter_cookie_secret
jupyterhub               | [I 2023-12-15 09:27:56.007 ServerApp] notebook_shim | extension was successfully linked.
jupyterhub               | [I 2023-12-15 09:27:56.036 ServerApp] notebook_shim | extension was successfully loaded.
jupyterhub               | [I 2023-12-15 09:27:56.039 ServerApp] jupyter_lsp | extension was successfully loaded.
jupyterhub               | [I 2023-12-15 09:27:56.040 ServerApp] jupyter_server_terminals | extension was successfully loaded.
jupyterhub               | [I 2023-12-15 09:27:56.058 JupyterHub log:191] 200 GET /hub/api (@172.20.0.3) 2.77ms
jupyterhub               | [I 2023-12-15 09:27:56.061 JupyterHubSingleUser] Updating Hub with activity every 300 seconds
jupyterhub               | [I 2023-12-15 09:27:56.062 ServerApp] jupyterhub | extension was successfully loaded.
jupyterhub               | [I 2023-12-15 09:27:56.068 LabApp] JupyterLab extension loaded from /usr/local/lib/python3.10/dist-packages/jupyterlab
jupyterhub               | [I 2023-12-15 09:27:56.069 LabApp] JupyterLab application directory is /usr/local/share/jupyter/lab
jupyterhub               | [I 2023-12-15 09:27:56.073 LabApp] Extension Manager is 'pypi'.
jupyterhub               | [I 2023-12-15 09:27:56.077 ServerApp] jupyterlab | extension was successfully loaded.
jupyterhub               | [I 2023-12-15 09:27:56.079 ServerApp] Serving notebooks from local directory: /home/test
jupyterhub               | [I 2023-12-15 09:27:56.080 ServerApp] Jupyter Server 2.12.1 is running at:
jupyterhub               | [I 2023-12-15 09:27:56.080 ServerApp] http://127.0.0.1:60345/user/test/lab?token=...
jupyterhub               | [I 2023-12-15 09:27:56.080 ServerApp]     http://127.0.0.1:60345/user/test/lab?token=...
jupyterhub               | [I 2023-12-15 09:27:56.080 ServerApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
jupyterhub               | [I 2023-12-15 09:27:56.180 JupyterHub log:191] 200 POST /hub/api/users/test/activity (test@172.20.0.3) 70.51ms
jupyterhub               | [I 2023-12-15 09:27:56.862 ServerApp] Skipped non-installed server(s): bash-language-server, dockerfile-language-server-nodejs, javascript-typescript-langserver, jedi-language-server, julia-language-server, pyright, python-language-server, python-lsp-server, r-languageserver, sql-language-server, texlab, typescript-language-server, unified-language-server, vscode-css-languageserver-bin, vscode-html-languageserver-bin, vscode-json-languageserver-bin, yaml-language-server
jupyterhub               | [I 2023-12-15 09:27:57.649 ServerApp] 302 GET /user/test/ -> /user/test/lab? (@127.0.0.1) 0.50ms
jupyterhub               | [W 2023-12-15 09:27:57.649 JupyterHub _version:37] Single-user server has no version header, which means it is likely < 0.8. Expected 4.0.2
jupyterhub               | [I 2023-12-15 09:27:57.649 JupyterHub base:990] User test took 6.041 seconds to start
jupyterhub               | [I 2023-12-15 09:27:57.650 JupyterHub proxy:330] Adding user test to proxy /user/test/ => http://127.0.0.1:60345
jupyterhub               | 09:27:57.652 [ConfigProxy] info: Adding route /user/test -> http://127.0.0.1:60345
jupyterhub               | 09:27:57.652 [ConfigProxy] info: Route added /user/test -> http://127.0.0.1:60345
jupyterhub               | 09:27:57.653 [ConfigProxy] info: 201 POST /api/routes/user/test 
jupyterhub               | [I 2023-12-15 09:27:57.654 JupyterHub users:768] Server test is ready
jupyterhub               | [I 2023-12-15 09:27:57.655 JupyterHub log:191] 200 GET /hub/api/users/test/server/progress?_xsrf=[secret] (test@::ffff:172.20.0.1) 4967.51ms
jupyterhub               | [I 2023-12-15 09:27:57.781 JupyterHub log:191] 302 GET /hub/spawn-pending/test -> /user/test/ (test@::ffff:172.20.0.1) 3.35ms
jupyterhub               | [I 2023-12-15 09:27:57.833 ServerApp] 302 GET /user/test/ -> /user/test/lab? (@::ffff:172.20.0.1) 0.94ms
jupyterhub               | [I 2023-12-15 09:27:57.882 ServerApp] 302 GET /user/test/lab? -> /hub/api/oauth2/authorize?client_id=jupyterhub-user-test&redirect_uri=%2Fuser%2Ftest%2Foauth_callback&response_type=code&state=[secret] (@::ffff:172.20.0.1) 1.66ms
jupyterhub               | [I 2023-12-15 09:27:57.957 JupyterHub log:191] 302 GET /hub/api/oauth2/authorize?client_id=jupyterhub-user-test&redirect_uri=%2Fuser%2Ftest%2Foauth_callback&response_type=code&state=[secret] -> /user/test/oauth_callback?code=[secret]&state=[secret] (test@::ffff:172.20.0.1) 27.07ms
jupyterhub               | [I 2023-12-15 09:27:58.048 JupyterHub log:191] 200 POST /hub/api/oauth2/token (test@172.20.0.3) 68.04ms
jupyterhub               | [I 2023-12-15 09:27:58.066 JupyterHub log:191] 200 GET /hub/api/user (test@172.20.0.3) 15.62ms
jupyterhub               | [I 2023-12-15 09:27:58.067 ServerApp] Logged-in user {'admin': False, 'name': 'test', 'kind': 'user', 'groups': [], 'session_id': 'a75b5295a8104d2bb7b8a714e89a8ade', 'scopes': ['access:servers!server=test/', 'read:users:groups!user=test', 'read:users:name!user=test']}

Your jupyterhub config file is not loaded, as shown by these log lines:

jupyterhub               | [I 2023-12-15 09:27:27.882 JupyterHub app:2889] Using Authenticator: jupyterhub.auth.DummyAuthenticator-4.0.2
jupyterhub               | [I 2023-12-15 09:27:27.882 JupyterHub app:2889] Using Spawner: jupyterhub.spawner.LocalProcessSpawner-4.0.2

I believe that’s because you’ve placed it in /srv/jupyterhub, and then overridden that same directory with a volume for persistence.

If you instead put it in a non-clobbered directory:

COPY jupyterhub_config.py /etc/jupyterhub/jupyterhub_config.py
CMD jupyterhub -f /etc/jupyterhub/jupyterhub_config.py

Or better yet, place the jupyterhub_config.py into your persistent /srv/jupyterhub/ mount, e.g. with:

    volumes: # Mount host paths or named volumes.
    # Configuration for the single-user servers
      - /var/run/docker.sock:/var/run/docker.sock # Mount the Docker socket so that containers launched by the Hub can be run with the same privileges as the Hub itself.
      - ./jupyterhub-data:/srv/jupyterhub # Hub data persistence

and put jupyterhub_config.py in ./jupyterhub-data/jupyterhub_config.py

Side note: you don’t need jupyterlab in your jupyterhub image, since single-user sessions won’t run there (once it’s actually using DockerSpawner, that is). I’m guessing that was added due to seeing import errors in startup logs, when the cause was not using the intended DockerSpawner.

If you need more options than just the image, you can override options_form and options_from_form: Spawners — JupyterHub documentation

Thanks,
Mark

Dear minrk,

Thank you very much for identifying the key points. I have noticed that no matter how I change the jupyterhub_config.py, JupyterHub always uses LocalProcessSpawner.
I have a few questions about your solution, I can’t find the Docker configuration setting that you mentioned, which overrides the same directory with a volume for persistence.

Regarding your first solution, I mounted the /persist/ folder inside jupyterhub_config.py before docker-compose mounted the host folder into /persist. As a result, JupyterHub returned an error stating /persist folder not found.

For the better solution what I don’t understand. Do I only need to put the jupyterhub_config.py into jupyterhub-data/ without changing any configuration files? I have tried this before but JupyterHub still used LocalProcessSpawner.

Additionally, I have tried to build JupyterHub from the base Docker image “manually”, but I found that JupyterHub still uses LocalProcessSpawner. Could this be a problem with the operating system or package version?

Have you definitely mounted the config file into the container in the correct place? Can you show us your full setup?

All of my config files are up there.
I think the part of mount is

jupyterhub_config.py

c.DockerSpawner.notebook_dir = notebook_dir
c.DockerSpawner.volumes = {'/Users/apple/Desktop/jupyterhub/{username}': '/home/jovyan/work'}

c.JupyterHub.cookie_secret_file = '/persist/jupyterhub_cookie_secret'
c.JupyterHub.db_url = '/persist/jupyterhub.sqlite'

docker-compose.yaml

  jupyterhub:
  ...
	volumes:
	# Configuration for the single-user servers
	- /var/run/docker.sock:/var/run/docker.sock
	- .:/persist
	- ./jupyterhub_data:/srv/jupyterhub

volumes:
  jupyterhub_data:

and the directory structure is

.
├── Dockerfile
├── User
├── docker-compose.yaml
├── jupyterhub_data
│   └── jupyterhub_config.py
└── jupyterlab
    └── Dockerfile

My OS is MacOS Intel Core Sonoma 14.2.1

Set the command for the hub image to:

jupyterhub -f /srv/jupyterhub/jupyterhub_config.py

and double check the contents of that directory as seen inside the container (with ls, cat, etc. via docker exec)

Little late, but there is a typo:

“c.JupyrerHub.spawner_class = ‘dockerspawner.DockerSpawner’”
must be:
“c.JupyterHub.spawner_class = ‘dockerspawner.DockerSpawner’”

1 Like