Skip to content

Instantly share code, notes, and snippets.

@kathoef
Last active October 28, 2024 15:12
Show Gist options
  • Save kathoef/067f2feb197b0af4a165aa66eca6848c to your computer and use it in GitHub Desktop.
Save kathoef/067f2feb197b0af4a165aa66eca6848c to your computer and use it in GitHub Desktop.
Jupyterhub for Docker rootless terminal access

JupyterHub for Docker container course

This JupyterHub setup provides access to a web-based interactive docker terminal environment for use in e.g. Docker course settings. Issues around getting Docker to work on every participant's end device during the class are mitigated, and "bring your own working (!) Docker environment" is not anymore a requirement for taking part in the course. The setup ensures that teachers can focus solely on helping out with the course contents.

This setup is based on the "The Littlest JupyterHub" and leverages "Docker rootless" as provided by Docker Inc. to prevent issues around (1) privilege escalation (for sudo and docker-group Docker usage approaches) and (2) Docker daemon isolation. With this setup, every participant gets their own "safe to use" Docker container working environment, which also (mostly) resembles the Docker environment that would be presented on a local machine.

The setup was tested successfully with Ubuntu 22.04 and Ubuntu 24.04 releases.

Installation steps

Install Docker Engine according to the official docs,

sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin --no-install-recommends

Install rootless Docker according to the official docs,

sudo apt-get update && sudo apt-get install dbus-user-session uidmap docker-ce-rootless-extras

Install "The Littlest JupyterHub" according to the official docs,

sudo apt install python3 python3-dev git curl --no-install-recommends

curl -L https://tljh.jupyter.org/bootstrap.py | sudo -E python3 - --admin <admin-user-name>

Configure "The Littlest JupyterHub" to set /bin/bash as default login shell for users created by JupyterHub,

$ cat /opt/tljh/config/jupyterhub_config.d/jupyterhub_docker_rootless_shell.py
import subprocess


def docker_rootless_shell_pre_spawn_hook(spawner):
    """
    The usermod command fails for non-existing host users!
    This is because the UserCreatingSpawner has not yet created a non-existing user, when this hook function is called.

    https://github.com/jupyterhub/the-littlest-jupyterhub/blob/2.0.0/tljh/user_creating_spawner.py
    """
    host_username = "jupyter-" + spawner.user.name
    subprocess.check_call(["usermod", "--shell", "/bin/bash", host_username])


def docker_rootless_shell_progress_ready_hook(spawner, ready_event):
    """
    This hook function executes the usermod command multiple times during spawning of the Jupyter server.
    While the command fails during early spawning steps, it will succeed at least right before the Jupyter server is started for the user.
    Note, this could be considered a very dirty hack. However, it seems there are, currently, no other options to inject a successful usermod command via JupyterHub hook functions.
    Breaks only, if c.Spawner.progress_ready_hook is deprecated.

    Alternative implementation 1: Modify the useradd command in the ensure_user function in user.py which is imported by the UserCreatingSpawner.
    This is also "hacky", since we need to modify files provided by the TLJH installation.
    Might break for newer TLJH releases, or if the TLJH venv is updated.
    https://github.com/jupyterhub/the-littlest-jupyterhub/blob/2.0.0/tljh/user.py#L29

    Alternative implementation 2: Subclass the UserCreatingSpawner and patch the start() class method.
    This is also "hacky", since we would need to ensure that the patched class method is a drop-in replacement to the method provided by the TLJH installation.
    Might break for newer TLJH releases, or if the TLJH venv is updated.
    https://github.com/jupyterhub/the-littlest-jupyterhub/blob/2.0.0/tljh/user_creating_spawner.py
    """
    try:
        host_username = "jupyter-" + spawner.user.name
        subprocess.check_call(["usermod", "--shell", "/bin/bash", host_username])
    except:
        pass
    return ready_event


# c.Spawner.pre_spawn_hook = docker_rootless_shell_pre_spawn_hook
c.Spawner.progress_ready_hook = docker_rootless_shell_progress_ready_hook

Configure Bash hooks to provide user-friendly Docker rootless initiation,

printf "\nsource /opt/docker-rootless-hooks.sh\n" | sudo tee -a /etc/bash.bashrc
sudo chmod ugo+x /opt/docker-rootless-hooks.sh
$ cat /opt/docker-rootless-hooks.sh
#!/bin/bash

DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
export DOCKER_HOST

docker_alias () {
  echo "Please type 'docker_rootless' to use the Docker rootless daemon."
}

if [ -z ${SSH_TTY+y} ]; then
  alias docker=docker_alias
fi

docker_rootless () {

  if [ -z ${SSH_TTY+y} ]; then

    if [ ! -f "$HOME/.ssh/id_ed25519" ]; then
      echo "Creating SSH keys..."
      ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N ""
      printf "\n\n"; echo "Configuring authorized SSH keys..."
      cat "$HOME/.ssh/id_ed25519.pub" > "$HOME/.ssh/authorized_keys"
      cat "$HOME/.ssh/authorized_keys"; printf "\n"
    fi

    if [ ! -f "$HOME/.config/systemd/user/docker.service" ]; then
      echo "Installing Docker rootless daemon..."
      # shellcheck disable=SC2086
      ssh $USER@localhost "dockerd-rootless-setuptool.sh install; exit"
      echo "Please type 'docker_rootless' to use the Docker rootless daemon."
    else
      # shellcheck disable=SC2086
      ssh $USER@localhost
    fi

  else

    echo The Docker rootless daemon is already running...

  fi

}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment