feat: idle server culler for automatic shutdown of inactive servers

- Add jupyterhub-idle-culler package to Dockerfile
- Configure as managed JupyterHub service with role-based scopes
- Environment variables: IDLE_CULLER_ENABLED, IDLE_CULLER_TIMEOUT,
  IDLE_CULLER_CULL_EVERY, IDLE_CULLER_MAX_AGE
- Default: disabled, 24h timeout, 10min check interval
- Bump version to 3.6.0
This commit is contained in:
stellarshenson
2026-01-14 16:15:21 +01:00
parent cd9c6bf7fa
commit 4b6ac08ab0
5 changed files with 69 additions and 5 deletions

View File

@@ -138,3 +138,6 @@ This journal tracks substantive work on documents, diagrams, and documentation c
45. **Task - Selective notification recipients**: Enhanced notification broadcast to allow targeting specific servers<br>
**Result**: Added ActiveServersHandler (`GET /api/notifications/active-servers`) to list active servers. Modified BroadcastNotificationHandler to accept optional `recipients` array - filters to selected users if provided, sends to all if omitted (backward compatible). Updated notifications.html with "Send to all active servers" checkbox, server selection list with Select All/Deselect All buttons, dynamic button text showing recipient count. Validation prevents sending with no recipients selected
46. **Task - Idle server culler**: Implemented automatic shutdown of inactive servers<br>
**Result**: Added jupyterhub-idle-culler package to Dockerfile. Added configuration with environment variables: IDLE_CULLER_ENABLED (default 0), IDLE_CULLER_TIMEOUT (default 86400s/24h), IDLE_CULLER_CULL_EVERY (default 600s/10min), IDLE_CULLER_MAX_AGE (default 0/unlimited). Service runs as managed JupyterHub service with role-based scopes. Disabled by default, opt-in via IDLE_CULLER_ENABLED=1. Bumped version to 3.6.0

View File

@@ -18,6 +18,7 @@ Multi-user JupyterHub 4 deployment platform with data science stack, GPU support
- **Native Authentication**: Built-in user management with NativeAuthenticator supporting optional self-registration (`ENABLE_SIGNUP`) and admin approval. Authorization page protects existing users from accidental discard - only pending signup requests can be discarded
- **Admin User Creation**: Batch user creation from admin panel with auto-generated mnemonic passwords (e.g., `storm-apple-ocean`). Credentials modal with copy/download options
- **Shared Storage**: Optional CIFS/NAS mount support for shared datasets across all users
- **Idle Server Culler**: Automatic shutdown of inactive servers after configurable timeout (default: 24 hours). Frees resources when users leave servers running
- **Production Ready**: Traefik reverse proxy with TLS termination, automatic container updates via Watchtower
## User Interface
@@ -360,6 +361,24 @@ services:
- ENABLE_SIGNUP=0 # disable self-registration, admin creates users
```
#### Idle Server Culler
Automatically stop user servers after a period of inactivity to free up resources. Disabled by default.
```yaml
services:
jupyterhub:
environment:
- IDLE_CULLER_ENABLED=1 # enable idle culler
- IDLE_CULLER_TIMEOUT=86400 # 24 hours (default) - stop after this many seconds of inactivity
- IDLE_CULLER_CULL_EVERY=600 # 10 minutes (default) - how often to check for idle servers
- IDLE_CULLER_MAX_AGE=0 # 0 (default) - max server age regardless of activity (0=unlimited)
```
**Behavior**:
- `IDLE_CULLER_TIMEOUT`: Server is stopped after this many seconds without activity. Active servers are never culled
- `IDLE_CULLER_MAX_AGE`: Force stop servers older than this (useful to force image updates). Set to 0 to disable
#### Custom Branding
Replace the default JupyterHub logo with a custom logo. Mount your logo file and reference it via environment variable:

View File

@@ -188,7 +188,11 @@ ENABLE_GPU_SUPPORT = int(os.environ.get("ENABLE_GPU_SUPPORT", 2))
ENABLE_SERVICE_MLFLOW = int(os.environ.get("ENABLE_SERVICE_MLFLOW", 1))
ENABLE_SERVICE_GLANCES = int(os.environ.get("ENABLE_SERVICE_GLANCES", 1))
ENABLE_SERVICE_TENSORBOARD = int(os.environ.get("ENABLE_SERVICE_TENSORBOARD", 1))
ENABLE_SIGNUP = int(os.environ.get("ENABLE_SIGNUP", 1)) # 0 - disabled (admin creates users), 1 - enabled (self-registration)
ENABLE_SIGNUP = int(os.environ.get("ENABLE_SIGNUP", 1)) # 0 - disabled (admin creates users), 1 - enabled (self-registration)
IDLE_CULLER_ENABLED = int(os.environ.get("IDLE_CULLER_ENABLED", 0)) # 0 - disabled, 1 - enabled
IDLE_CULLER_TIMEOUT = int(os.environ.get("IDLE_CULLER_TIMEOUT", 86400)) # idle timeout in seconds (default: 24 hours)
IDLE_CULLER_CULL_EVERY = int(os.environ.get("IDLE_CULLER_CULL_EVERY", 600)) # check interval in seconds (default: 10 min)
IDLE_CULLER_MAX_AGE = int(os.environ.get("IDLE_CULLER_MAX_AGE", 0)) # max server age in seconds (0 = unlimited)
TF_CPP_MIN_LOG_LEVEL = int(os.environ.get("TF_CPP_MIN_LOG_LEVEL", 3))
DOCKER_NOTEBOOK_DIR = "/home/lab/workspace"
JUPYTERHUB_BASE_URL = os.environ.get("JUPYTERHUB_BASE_URL")
@@ -421,4 +425,41 @@ if c is not None:
(r'/notifications', NotificationsPageHandler),
]
# Idle culler service - automatically stops servers after inactivity
if IDLE_CULLER_ENABLED == 1:
import sys
# Define role with required scopes for idle culler
c.JupyterHub.load_roles = [
{
"name": "jupyterhub-idle-culler-role",
"scopes": [
"list:users",
"read:users:activity",
"read:servers",
"delete:servers",
],
"services": ["jupyterhub-idle-culler"],
}
]
# Configure idle culler service
culler_cmd = [
sys.executable,
"-m", "jupyterhub_idle_culler",
f"--timeout={IDLE_CULLER_TIMEOUT}",
f"--cull-every={IDLE_CULLER_CULL_EVERY}",
]
if IDLE_CULLER_MAX_AGE > 0:
culler_cmd.append(f"--max-age={IDLE_CULLER_MAX_AGE}")
c.JupyterHub.services = [
{
"name": "jupyterhub-idle-culler",
"command": culler_cmd,
}
]
print(f"[Idle Culler] Enabled - timeout={IDLE_CULLER_TIMEOUT}s, check every={IDLE_CULLER_CULL_EVERY}s, max_age={IDLE_CULLER_MAX_AGE}s")
# EOF

View File

@@ -3,8 +3,8 @@ PROJECT_NAME="stellars-jupyterhub-ds"
PROJECT_DESCRIPTION="Multi-user JupyterHub 4 deployment platform with data science stack, GPU auto-detection, NativeAuthenticator, and isolated per-user environments spawned via DockerSpawner"
# Version
VERSION="3.5.46_cuda-12.9.1_jh-5.4.2"
VERSION_COMMENT="Admin user creation with auto-generated passwords, NativeAuth sync, custom templates"
VERSION="3.6.0_cuda-12.9.1_jh-5.4.2"
VERSION_COMMENT="Idle server culler, selective notifications, volume encoding fix"
RELEASE_TAG="RELEASE_3.2.11"
RELEASE_DATE="2025-11-09"

View File

@@ -43,11 +43,12 @@ COPY --chmod=644 services/jupyterhub/templates_enhanced/*.html /srv/jupyterhub/
COPY --chmod=644 services/jupyterhub/templates_enhanced/static/custom.css /tmp/custom.css
COPY --chmod=644 config/jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py
## install dockerspawner, nativeauthenticator
## install dockerspawner, nativeauthenticator, idle-culler
RUN pip install -U --no-cache-dir \
docker \
dockerspawner \
jupyterhub-nativeauthenticator
jupyterhub-nativeauthenticator \
jupyterhub-idle-culler
## copy custom.css to JupyterHub's static directory
RUN <<-EOF