researchcloud-items

Installation scripts for SURF ResearchCloud catalog components

View the Project on GitHub UtrechtUniversity/researchcloud-items

Role jupyterhub_standalone_proxy

back to index

Summary

Use JupyterHub’s proxy to spawn and serve an arbitrary webapplication (instead of a Jupyter Notebook), running as any user that logs in to the hub. This allows you to e.g. serve an IDE running as user foo when foo logs in, and an IDE running as user bar when bar logs in. Effectively, this role provides a generic way of serving apps to users running under their own user account.

This role can also be called multiple times (e.g. by multiple components) to serve multiple different standalone apps.

This role uses the JupyterHub role, so it supports SRAM authentication.

The role uses the jupyter-server-proxy standalone plugin, so that instead of getting the Hub webpage and having to select a web application, users are immediately served the desired webapplication after logging in.

Requires

Description

This role levarages JupyterHub’s proxy to spawn and serve an arbitrary webapplication (instead of a Jupyter Notebook), running as a specific user. To see how you can determine which application should be spawned, see the examples.

The role uses the JupyterHub role, so it supports SRAM authentication. It modifies the JupyterHub to replace the default command that is spawned by the Hub: instead of running a notebook server, the jupyter-standalone-proxy command will be executed (via the sudospawner command), with the config file provided.

See the examples below for illustration of how to configure this role.

Variables

Examples

There are two ways in which jupyterhub-standaloneproxy can be configured with this role:

Configuration option 1

Set the command to be run by jupyter-standaloneproxy directly.

By setting the jhsp_standalone_proxy_cmd_args variable, you can directly determine which arguments will be passed along to the jupyter-standaloneproxy command.

These arguments will be specified in the sudospawner-singleuser script that the role places in the JupyterHub venv.

The following configuration, for example, would launch a simple HTTP fileserver as a standalone app:

roles:
  - role: jupyterhub_standalone_proxy
    vars:
      jhsp_standalone_proxy_cmd_args:
        # - "--no-authentication" # optional arguments for jupyterhub-standaloneproxy here. See [here](https://jupyter-server-proxy.readthedocs.io/en/latest/standalone.html) for available options.
        - "--" # marks the end of options for jupyterhub-standaloneproxy, after this point we specify the command that should be launched as a standalone proxy.
        - "/usr/bin/python3"
        - "-m"
        - "http.server"
        - "{port}"
        # The above command will spawn the default python HTTP fileserver, running in the user's homedirectory.

With this method, you can serve only one app as a standalone proxy (i.e., the provided command).

Configuration option 2

Place a config file in the standalone config directory.

Alternatively, you can configure jupyter-standaloneproxy through a Python configuration file with traitlets. Any files placed in the directory specified by the jhsp_standalone_proxy_config_dir variable that end in the extension .conf.py will be automatically detected by the JupyterHub configuration.

See here for available configuration options.

This gives you two possibilities:

You can place config files:

Here is an example of a valid config file:

# example.conf.py in /usr/local/etc/jupyter/standalone
c.StandaloneProxyServer.command = [
    "/usr/bin/python3", "-m", "http.server", "{port}"
]

Here is an example of how to install this configuration file by calling this role:

roles:
  - role: jupyterhub_standalone_proxy
    vars:
      jhsp_standalone_proxy_config: |
        c.StandaloneProxyServer.command = [
          "/usr/bin/python3", "-m", "http.server", "{port}"
        ]

Hosting multiple standalone apps

If you place multiple traitlet config files in the appropriate directory, the Hub is configured to allow the user to spawn any of the apps configured in this file. That is, given that both example1.conf.py and example2.conf.py are in the appropriate directory, when logging in to the Hub, the user will be presented with a choice to either spawn the app configured in example1 or in example2.

This means multiple components can call this role and install their own configuration files, whereup the Hub will serve both apps.

The user can also spawn both servers, which will be served under different URLs, if jhsp_named_servers is enabled. By default, the user can access all the apps spawned by them (and spawn new ones) under the JupyterHub ‘home’ view (http://myinstance.com/hub/home).

In addition to letting the user choose which app to spawn (done by this role), you could provide your own logic (e.g. in a custom webpage) to conditionally serve different apps (e.g. depending on the URL path). At this point, this is not done by this role.

A note on handling CORS

In a standard JupyterHub, the Hub will start JupyterLab, and JupyterLab will start various users’ servers. jupyter-standaloneproxy removes the need for JupyterLab, but this can lead to some complications. One such complication is that JuptyerLab will normally ensure that CORS is properly configured for the servers it starts. This means that applications will know that requests from the users’ browser, which accesses the server at the workspace’s FQDN, should be allowed—even though the application runs on a different domain (namely, localhost).

In the case of standalone apps, we need to do some work to properly configure CORS ourselves—although this will not be a problem with all applications. Specifically, it will only be required for proxied apps that use websockets in the frontend. Just test out your app to see if you run into any frontend issues due to CORS. If so, you can use the following function from utils.py in your traitlet config file:

from utils import * # import from utils.py, placed in the same directory as the config files (/usr/local/etc/jupyter/standalone by default)

custom_check_origin_patch() # apply monkeypatch for custom CORS allowlist for websockets from utils.py

# rest of your traitlets config here, e.g.:
#c.StandaloneProxyServer.command =  [
  # Your command here
#]

See the custom_check_origin_patch() function in utils.py for technical details.

How it works

sudospawner

JupyterHub’s sudospawner is used to run the desired webapp as a specific user: when you login with user foo (usin e.g. SRAM), the hub (which runs as a dedicated service user itself) will use special sudo permissions to spawn the webapp running as user foo on the workspace. Since sudospawner by default launches the jupyter-singleuser command (used to start JupyterLab), we need to overwrite this command by placing a custom sudospawner-singleuser script in the Jupyter venv, as explained here. The custom script launches the jupyter-standaloneproxy instead of jupyter-singleuser.

Only users in the group specified by jupyterhub_allowed_users_group variable are allowed to use sudospawner in this way.

utils.py

This role will create utils.py in the directory configured by jhsp_standalone_proxy_config_dir. utils.py contains helpful Python functions that can be called from the traitlet config files (see above). This is to provide common functionality that is likely needed when configuring standalone apps, e.g.:

utils.py also contains the get_standalone_proxy_config function, which is used in the JupyterHub configuration to detect the presence of one or multiple config files (see above).

Since utils.py is by default placed in the same directory (=python package) as your traitlet config files, you can directly import it from your config file.

See also

History

2025 Written by Dawa Ometto (Utrecht University)

back to index