A multi-user server for Jupyter notebooks that provides authentication, spawning, and proxying for multiple users simultaneously
—
The JupyterHub spawner system is responsible for creating, managing, and stopping single-user Jupyter notebook servers. It provides a pluggable architecture supporting local processes, containers, cloud platforms, and custom deployment scenarios.
The foundation class for all spawner implementations in JupyterHub.
class Spawner(LoggingConfigurable):
"""
Base class for spawning single-user notebook servers.
Subclass this to implement custom spawning mechanisms.
"""
# User and server information
user: User # The user this spawner belongs to
name: str # The name of the server (empty string for default server)
server: Server # The server object from the database
# Configuration attributes
start_timeout: int # Timeout for server startup (seconds)
http_timeout: int # Timeout for HTTP requests to server
poll_interval: int # Interval for polling server status
# Server connection info
ip: str # IP address server should bind to
port: int # Port server should bind to
url: str # Full URL to the server
# Environment and resources
environment: Dict[str, str] # Environment variables
cmd: List[str] # Command to start server
args: List[str] # Arguments to the command
async def start(self):
"""
Start the single-user server.
Returns:
(ip, port) tuple of where the server is listening,
or just port if ip is unchanged
"""
async def stop(self, now=False):
"""
Stop the single-user server.
Args:
now: If True, force immediate stop without graceful shutdown
"""
async def poll(self):
"""
Poll the spawned process to see if it is still running.
Returns:
None if still running, integer exit code if stopped
"""
def get_state(self):
"""
Get the current state of the spawner.
Returns:
Dictionary of state information for persistence
"""
def load_state(self, state):
"""
Load state from a previous session.
Args:
state: Dictionary of state information
"""
def clear_state(self):
"""
Clear any stored state about the server.
"""
def get_env(self):
"""
Get the complete environment for the server.
Returns:
Dictionary of environment variables
"""
def get_args(self):
"""
Get the complete argument list for starting the server.
Returns:
List of command arguments
"""Spawners that run single-user servers as local system processes.
class LocalProcessSpawner(Spawner):
"""
Spawner that runs single-user servers as local processes.
The default spawner implementation for JupyterHub.
"""
# Process management
proc: subprocess.Popen # The subprocess object
pid: int # Process ID of the spawned server
# Local user management
create_system_users: bool # Whether to create system users
shell_cmd: List[str] # Shell command for process execution
async def start(self):
"""
Start server as local subprocess.
Returns:
(ip, port) tuple where server is listening
"""
async def stop(self, now=False):
"""
Stop the local process.
Args:
now: If True, send SIGKILL instead of SIGTERM
"""
async def poll(self):
"""
Poll the local process.
Returns:
None if running, exit code if stopped
"""
def make_preexec_fn(self, name):
"""
Create preexec function for subprocess.
Args:
name: Username to switch to
Returns:
Function to call before exec in subprocess
"""
class SimpleLocalProcessSpawner(LocalProcessSpawner):
"""
Simplified local process spawner.
Doesn't switch users or create system users. Suitable for
single-user or development deployments.
"""
# Simplified configuration
create_system_users: bool = False
shell_cmd: List[str] = [] # No shell wrapper# jupyterhub_config.py
c = get_config()
# Use local process spawner (default)
c.JupyterHub.spawner_class = 'localprocess'
# Configure spawner settings
c.Spawner.start_timeout = 60
c.Spawner.http_timeout = 30
# Set notebook directory
c.Spawner.notebook_dir = '/home/{username}/notebooks'
# Configure environment variables
c.Spawner.environment = {
'JUPYTER_ENABLE_LAB': 'yes',
'JUPYTERHUB_SINGLEUSER_APP': 'jupyter_server.serverapp.ServerApp'
}from jupyterhub.spawner import LocalProcessSpawner
import docker
class DockerSpawner(LocalProcessSpawner):
"""Example Docker-based spawner"""
# Docker configuration
image: str = 'jupyter/base-notebook'
remove: bool = True
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.client = docker.from_env()
self.container = None
async def start(self):
"""Start server in Docker container"""
# Docker container startup logic
self.container = self.client.containers.run(
image=self.image,
command=self.get_args(),
environment=self.get_env(),
ports={'8888/tcp': None},
detach=True,
remove=self.remove,
name=f'jupyter-{self.user.name}-{self.name}'
)
# Get container port mapping
port_info = self.container.attrs['NetworkSettings']['Ports']['8888/tcp'][0]
return (self.ip, int(port_info['HostPort']))
async def stop(self, now=False):
"""Stop Docker container"""
if self.container:
self.container.stop()
self.container = None
async def poll(self):
"""Poll container status"""
if not self.container:
return 1
self.container.reload()
status = self.container.status
if status == 'running':
return None
else:
return 1
# Register the spawner
c.JupyterHub.spawner_class = DockerSpawner
c.DockerSpawner.image = 'jupyter/scipy-notebook'class ResourceManagedSpawner(LocalProcessSpawner):
"""Spawner with resource limits"""
mem_limit: str = '1G' # Memory limit
cpu_limit: float = 1.0 # CPU limit
def get_env(self):
"""Add resource limits to environment"""
env = super().get_env()
env.update({
'MEM_LIMIT': self.mem_limit,
'CPU_LIMIT': str(self.cpu_limit)
})
return env
async def start(self):
"""Start with resource constraints"""
# Set resource limits before starting
self.set_resource_limits()
return await super().start()
def set_resource_limits(self):
"""Apply resource limits to the process"""
# Implementation depends on deployment method
pass
# Configuration with resource management
c.JupyterHub.spawner_class = ResourceManagedSpawner
c.ResourceManagedSpawner.mem_limit = '2G'
c.ResourceManagedSpawner.cpu_limit = 2.0class ProfileSpawner(LocalProcessSpawner):
"""Spawner with selectable profiles"""
profiles = [
{
'display_name': 'Basic Python',
'description': 'Basic Python environment',
'default': True,
'kubespawner_override': {
'image': 'jupyter/base-notebook',
'mem_limit': '1G',
'cpu_limit': 1
}
},
{
'display_name': 'Data Science',
'description': 'Full data science stack',
'kubespawner_override': {
'image': 'jupyter/datascience-notebook',
'mem_limit': '4G',
'cpu_limit': 2
}
}
]
def options_from_form(self, formdata):
"""Process spawner options from form"""
profile = formdata.get('profile', [None])[0]
return {'profile': profile}
def load_user_options(self, user_options):
"""Load user-selected options"""
profile_name = user_options.get('profile')
if profile_name:
# Apply profile configuration
profile = next(p for p in self.profiles if p['display_name'] == profile_name)
for key, value in profile.get('kubespawner_override', {}).items():
setattr(self, key, value)class StatefulSpawner(LocalProcessSpawner):
"""Spawner that persists state across restarts"""
def get_state(self):
"""Get spawner state for persistence"""
state = super().get_state()
state.update({
'custom_setting': self.custom_setting,
'last_activity': self.last_activity.isoformat()
})
return state
def load_state(self, state):
"""Load persisted state"""
super().load_state(state)
self.custom_setting = state.get('custom_setting')
if 'last_activity' in state:
self.last_activity = datetime.fromisoformat(state['last_activity'])class MonitoredSpawner(LocalProcessSpawner):
"""Spawner with health monitoring"""
async def poll(self):
"""Enhanced polling with health checks"""
# Check process status
status = await super().poll()
if status is not None:
return status
# Additional health checks
if not await self.health_check():
# Server is running but unhealthy
await self.restart_unhealthy_server()
return None
async def health_check(self):
"""Check if server is healthy"""
try:
# Make HTTP request to server
async with aiohttp.ClientSession() as session:
async with session.get(f'{self.url}/api/status') as resp:
return resp.status == 200
except:
return False
async def restart_unhealthy_server(self):
"""Restart an unhealthy server"""
await self.stop()
await self.start()class HookedSpawner(LocalProcessSpawner):
"""Spawner with pre/post hooks"""
async def start(self):
"""Start with pre/post hooks"""
await self.pre_spawn_hook()
try:
result = await super().start()
await self.post_spawn_hook()
return result
except Exception as e:
await self.spawn_error_hook(e)
raise
async def pre_spawn_hook(self):
"""Hook called before spawning"""
# Setup user workspace, check resources, etc.
pass
async def post_spawn_hook(self):
"""Hook called after successful spawn"""
# Register with external systems, send notifications, etc.
pass
async def spawn_error_hook(self, error):
"""Hook called on spawn error"""
# Log error, cleanup resources, send alerts, etc.
passInstall with Tessl CLI
npx tessl i tessl/pypi-jupyterhub