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.
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
}