or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication.mdconfiguration-utilities.mdcore-application.mddatabase-models.mdindex.mdmonitoring-metrics.mdrbac-permissions.mdrest-api.mdservices-oauth.mdsingleuser-integration.mdspawners.md

spawners.mddocs/

0

# Spawner System

1

2

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.

3

4

## Capabilities

5

6

### Base Spawner Class

7

8

The foundation class for all spawner implementations in JupyterHub.

9

10

```python { .api }

11

class Spawner(LoggingConfigurable):

12

"""

13

Base class for spawning single-user notebook servers.

14

15

Subclass this to implement custom spawning mechanisms.

16

"""

17

18

# User and server information

19

user: User # The user this spawner belongs to

20

name: str # The name of the server (empty string for default server)

21

server: Server # The server object from the database

22

23

# Configuration attributes

24

start_timeout: int # Timeout for server startup (seconds)

25

http_timeout: int # Timeout for HTTP requests to server

26

poll_interval: int # Interval for polling server status

27

28

# Server connection info

29

ip: str # IP address server should bind to

30

port: int # Port server should bind to

31

url: str # Full URL to the server

32

33

# Environment and resources

34

environment: Dict[str, str] # Environment variables

35

cmd: List[str] # Command to start server

36

args: List[str] # Arguments to the command

37

38

async def start(self):

39

"""

40

Start the single-user server.

41

42

Returns:

43

(ip, port) tuple of where the server is listening,

44

or just port if ip is unchanged

45

"""

46

47

async def stop(self, now=False):

48

"""

49

Stop the single-user server.

50

51

Args:

52

now: If True, force immediate stop without graceful shutdown

53

"""

54

55

async def poll(self):

56

"""

57

Poll the spawned process to see if it is still running.

58

59

Returns:

60

None if still running, integer exit code if stopped

61

"""

62

63

def get_state(self):

64

"""

65

Get the current state of the spawner.

66

67

Returns:

68

Dictionary of state information for persistence

69

"""

70

71

def load_state(self, state):

72

"""

73

Load state from a previous session.

74

75

Args:

76

state: Dictionary of state information

77

"""

78

79

def clear_state(self):

80

"""

81

Clear any stored state about the server.

82

"""

83

84

def get_env(self):

85

"""

86

Get the complete environment for the server.

87

88

Returns:

89

Dictionary of environment variables

90

"""

91

92

def get_args(self):

93

"""

94

Get the complete argument list for starting the server.

95

96

Returns:

97

List of command arguments

98

"""

99

```

100

101

### Local Process Spawners

102

103

Spawners that run single-user servers as local system processes.

104

105

```python { .api }

106

class LocalProcessSpawner(Spawner):

107

"""

108

Spawner that runs single-user servers as local processes.

109

110

The default spawner implementation for JupyterHub.

111

"""

112

113

# Process management

114

proc: subprocess.Popen # The subprocess object

115

pid: int # Process ID of the spawned server

116

117

# Local user management

118

create_system_users: bool # Whether to create system users

119

shell_cmd: List[str] # Shell command for process execution

120

121

async def start(self):

122

"""

123

Start server as local subprocess.

124

125

Returns:

126

(ip, port) tuple where server is listening

127

"""

128

129

async def stop(self, now=False):

130

"""

131

Stop the local process.

132

133

Args:

134

now: If True, send SIGKILL instead of SIGTERM

135

"""

136

137

async def poll(self):

138

"""

139

Poll the local process.

140

141

Returns:

142

None if running, exit code if stopped

143

"""

144

145

def make_preexec_fn(self, name):

146

"""

147

Create preexec function for subprocess.

148

149

Args:

150

name: Username to switch to

151

152

Returns:

153

Function to call before exec in subprocess

154

"""

155

156

class SimpleLocalProcessSpawner(LocalProcessSpawner):

157

"""

158

Simplified local process spawner.

159

160

Doesn't switch users or create system users. Suitable for

161

single-user or development deployments.

162

"""

163

164

# Simplified configuration

165

create_system_users: bool = False

166

shell_cmd: List[str] = [] # No shell wrapper

167

```

168

169

## Usage Examples

170

171

### Basic Local Process Spawner

172

173

```python

174

# jupyterhub_config.py

175

c = get_config()

176

177

# Use local process spawner (default)

178

c.JupyterHub.spawner_class = 'localprocess'

179

180

# Configure spawner settings

181

c.Spawner.start_timeout = 60

182

c.Spawner.http_timeout = 30

183

184

# Set notebook directory

185

c.Spawner.notebook_dir = '/home/{username}/notebooks'

186

187

# Configure environment variables

188

c.Spawner.environment = {

189

'JUPYTER_ENABLE_LAB': 'yes',

190

'JUPYTERHUB_SINGLEUSER_APP': 'jupyter_server.serverapp.ServerApp'

191

}

192

```

193

194

### Custom Spawner Implementation

195

196

```python

197

from jupyterhub.spawner import LocalProcessSpawner

198

import docker

199

200

class DockerSpawner(LocalProcessSpawner):

201

"""Example Docker-based spawner"""

202

203

# Docker configuration

204

image: str = 'jupyter/base-notebook'

205

remove: bool = True

206

207

def __init__(self, **kwargs):

208

super().__init__(**kwargs)

209

self.client = docker.from_env()

210

self.container = None

211

212

async def start(self):

213

"""Start server in Docker container"""

214

# Docker container startup logic

215

self.container = self.client.containers.run(

216

image=self.image,

217

command=self.get_args(),

218

environment=self.get_env(),

219

ports={'8888/tcp': None},

220

detach=True,

221

remove=self.remove,

222

name=f'jupyter-{self.user.name}-{self.name}'

223

)

224

225

# Get container port mapping

226

port_info = self.container.attrs['NetworkSettings']['Ports']['8888/tcp'][0]

227

return (self.ip, int(port_info['HostPort']))

228

229

async def stop(self, now=False):

230

"""Stop Docker container"""

231

if self.container:

232

self.container.stop()

233

self.container = None

234

235

async def poll(self):

236

"""Poll container status"""

237

if not self.container:

238

return 1

239

240

self.container.reload()

241

status = self.container.status

242

243

if status == 'running':

244

return None

245

else:

246

return 1

247

248

# Register the spawner

249

c.JupyterHub.spawner_class = DockerSpawner

250

c.DockerSpawner.image = 'jupyter/scipy-notebook'

251

```

252

253

### Resource Management

254

255

```python

256

class ResourceManagedSpawner(LocalProcessSpawner):

257

"""Spawner with resource limits"""

258

259

mem_limit: str = '1G' # Memory limit

260

cpu_limit: float = 1.0 # CPU limit

261

262

def get_env(self):

263

"""Add resource limits to environment"""

264

env = super().get_env()

265

env.update({

266

'MEM_LIMIT': self.mem_limit,

267

'CPU_LIMIT': str(self.cpu_limit)

268

})

269

return env

270

271

async def start(self):

272

"""Start with resource constraints"""

273

# Set resource limits before starting

274

self.set_resource_limits()

275

return await super().start()

276

277

def set_resource_limits(self):

278

"""Apply resource limits to the process"""

279

# Implementation depends on deployment method

280

pass

281

282

# Configuration with resource management

283

c.JupyterHub.spawner_class = ResourceManagedSpawner

284

c.ResourceManagedSpawner.mem_limit = '2G'

285

c.ResourceManagedSpawner.cpu_limit = 2.0

286

```

287

288

### Profile-based Spawning

289

290

```python

291

class ProfileSpawner(LocalProcessSpawner):

292

"""Spawner with selectable profiles"""

293

294

profiles = [

295

{

296

'display_name': 'Basic Python',

297

'description': 'Basic Python environment',

298

'default': True,

299

'kubespawner_override': {

300

'image': 'jupyter/base-notebook',

301

'mem_limit': '1G',

302

'cpu_limit': 1

303

}

304

},

305

{

306

'display_name': 'Data Science',

307

'description': 'Full data science stack',

308

'kubespawner_override': {

309

'image': 'jupyter/datascience-notebook',

310

'mem_limit': '4G',

311

'cpu_limit': 2

312

}

313

}

314

]

315

316

def options_from_form(self, formdata):

317

"""Process spawner options from form"""

318

profile = formdata.get('profile', [None])[0]

319

return {'profile': profile}

320

321

def load_user_options(self, user_options):

322

"""Load user-selected options"""

323

profile_name = user_options.get('profile')

324

if profile_name:

325

# Apply profile configuration

326

profile = next(p for p in self.profiles if p['display_name'] == profile_name)

327

for key, value in profile.get('kubespawner_override', {}).items():

328

setattr(self, key, value)

329

```

330

331

## Advanced Patterns

332

333

### State Persistence

334

335

```python

336

class StatefulSpawner(LocalProcessSpawner):

337

"""Spawner that persists state across restarts"""

338

339

def get_state(self):

340

"""Get spawner state for persistence"""

341

state = super().get_state()

342

state.update({

343

'custom_setting': self.custom_setting,

344

'last_activity': self.last_activity.isoformat()

345

})

346

return state

347

348

def load_state(self, state):

349

"""Load persisted state"""

350

super().load_state(state)

351

self.custom_setting = state.get('custom_setting')

352

if 'last_activity' in state:

353

self.last_activity = datetime.fromisoformat(state['last_activity'])

354

```

355

356

### Health Monitoring

357

358

```python

359

class MonitoredSpawner(LocalProcessSpawner):

360

"""Spawner with health monitoring"""

361

362

async def poll(self):

363

"""Enhanced polling with health checks"""

364

# Check process status

365

status = await super().poll()

366

if status is not None:

367

return status

368

369

# Additional health checks

370

if not await self.health_check():

371

# Server is running but unhealthy

372

await self.restart_unhealthy_server()

373

374

return None

375

376

async def health_check(self):

377

"""Check if server is healthy"""

378

try:

379

# Make HTTP request to server

380

async with aiohttp.ClientSession() as session:

381

async with session.get(f'{self.url}/api/status') as resp:

382

return resp.status == 200

383

except:

384

return False

385

386

async def restart_unhealthy_server(self):

387

"""Restart an unhealthy server"""

388

await self.stop()

389

await self.start()

390

```

391

392

### Pre/Post Hooks

393

394

```python

395

class HookedSpawner(LocalProcessSpawner):

396

"""Spawner with pre/post hooks"""

397

398

async def start(self):

399

"""Start with pre/post hooks"""

400

await self.pre_spawn_hook()

401

402

try:

403

result = await super().start()

404

await self.post_spawn_hook()

405

return result

406

except Exception as e:

407

await self.spawn_error_hook(e)

408

raise

409

410

async def pre_spawn_hook(self):

411

"""Hook called before spawning"""

412

# Setup user workspace, check resources, etc.

413

pass

414

415

async def post_spawn_hook(self):

416

"""Hook called after successful spawn"""

417

# Register with external systems, send notifications, etc.

418

pass

419

420

async def spawn_error_hook(self, error):

421

"""Hook called on spawn error"""

422

# Log error, cleanup resources, send alerts, etc.

423

pass

424

```