JupyterHub Deployment
This guide covers deploying JupyterHub with marimo support on Ubuntu/Debian using uv and systemd.
For comprehensive JupyterHub documentation, see the official tutorial.
Prerequisites
- Fresh Ubuntu/Debian server with root access
- A domain name pointing to your server
Quick DNS
sslip.io provides free DNS that maps <IP>.sslip.io to your IP, useful for SSL certificates without a domain.
Part 1: System Setup
Install Dependencies
sudo su
apt update
apt install -y nodejs npm nginx certbot python3-certbot-nginx
curl -LsSf https://astral.sh/uv/install.sh | sh
Create System Users
# jupyterhub: Runs the hub process (control plane)
useradd -r -m -s /bin/bash jupyterhub
# jupyter: Runs spawned notebook servers (user workloads)
useradd -r -s /usr/sbin/nologin jupyter
Create Directories
mkdir -p /opt/jupyterhub /opt/notebooks /opt/bin
chown jupyterhub:jupyterhub /opt/jupyterhub
chown jupyter:jupyter /opt/notebooks
Part 2: JupyterHub Installation
As the jupyterhub user:
Edit pyproject.toml:
[project]
name = "jupyterhub-deployment"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"jupyterhub>=5.4.3",
"jupyterlab>=4.5.1",
"notebook>=7.5.0",
"oauthenticator>=17.3.0",
"jupyterhub-systemdspawner>=1.0.2",
"marimo>=0.19.8",
"marimo-jupyter-extension>=0.1.0",
]
GitHub OAuth Setup
- Go to GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
- Set callback URL to
https://your-domain.com/hub/oauth_callback - Note your Client ID and Client Secret
JupyterHub Configuration
Create jupyterhub_config.py:
import os
from oauthenticator.github import GitHubOAuthenticator
# Authentication
c.JupyterHub.authenticator_class = GitHubOAuthenticator
c.GitHubOAuthenticator.client_id = os.environ.get("GITHUB_CLIENT_ID")
c.GitHubOAuthenticator.client_secret = os.environ.get("GITHUB_CLIENT_SECRET")
c.GitHubOAuthenticator.oauth_callback_url = os.environ.get("OAUTH_CALLBACK_URL")
c.GitHubOAuthenticator.allowed_users = {"your-github-username"}
c.GitHubOAuthenticator.scope = ["read:org"]
# Spawner
c.JupyterHub.spawner_class = "systemdspawner.SystemdSpawner"
c.SystemdSpawner.user = "jupyter"
c.SystemdSpawner.username_template = "jupyter"
c.SystemdSpawner.user_workingdir = "/opt/notebooks/{USERNAME}"
# HTTP Proxy
c.ConfigurableHTTPProxy.command = "/opt/jupyterhub/node_modules/configurable-http-proxy/bin/configurable-http-proxy"
# Spawned notebook environment
c.SystemdSpawner.environment = {
"PATH": "/opt/jupyterhub/.venv/bin:/opt/bin:/usr/local/bin:/usr/bin:/bin",
"XDG_RUNTIME_DIR": "/run/user/jupyter",
"XDG_DATA_HOME": "/opt/notebooks/.local/share",
"XDG_CONFIG_HOME": "/opt/notebooks/.config",
"XDG_CACHE_HOME": "/opt/notebooks/.cache",
"HOME": "/opt/notebooks",
}
Part 3: SSL & Reverse Proxy
nginx Configuration
Create /opt/jupyterhub/nginx.conf:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
As root:
ln -sf /opt/jupyterhub/nginx.conf /etc/nginx/sites-enabled/jupyterhub
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
certbot --nginx -d your-domain.com --non-interactive --agree-tos -m your@email.com
echo "0 0 * * * root certbot renew --quiet" > /etc/cron.d/certbot-renew
Part 4: systemd Service
Create /etc/systemd/system/jupyterhub.service:
[Unit]
Description=JupyterHub
After=syslog.target network.target
[Service]
User=jupyterhub
WorkingDirectory=/opt/jupyterhub
Environment="GITHUB_CLIENT_ID=your-client-id"
Environment="GITHUB_CLIENT_SECRET=your-client-secret"
Environment="OAUTH_CALLBACK_URL=https://your-domain.com/hub/oauth_callback"
ExecStart=/opt/jupyterhub/.venv/bin/jupyterhub -f /opt/jupyterhub/jupyterhub_config.py
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
Enable and start:
Verification
Visit https://your-domain.com and authenticate with GitHub.
Using marimo
- Log into JupyterHub
- Start your server
- Click the marimo icon in the JupyterLab launcher, or navigate to
/user/<username>/marimo/
marimo notebooks are stored in the user's working directory (/opt/notebooks/<username>/).