diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index 1aafe20..f2155c4 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -64,17 +64,17 @@ This Python configuration file controls all JupyterHub behavior:
**Environment Variables** (set in compose.yml or compose_override.yml):
- `JUPYTERHUB_ADMIN`: Admin username (default: `admin`)
-- `DOCKER_NOTEBOOK_IMAGE`: JupyterLab image to spawn (default: `stellars/stellars-jupyterlab-ds:latest`)
-- `DOCKER_NETWORK_NAME`: Network for spawned containers (default: `jupyterhub_network`)
+- `JUPYTERHUB_NOTEBOOK_IMAGE`: JupyterLab image to spawn (default: `stellars/stellars-jupyterlab-ds:latest`)
+- `JUPYTERHUB_NETWORK_NAME`: Network for spawned containers (default: `jupyterhub_network`)
- `JUPYTERHUB_BASE_URL`: URL prefix (default: `/jupyterhub`)
-- `ENABLE_GPU_SUPPORT`: GPU mode - `0` (disabled), `1` (enabled), `2` (auto-detect)
-- `ENABLE_JUPYTERHUB_SSL`: Direct SSL config - `0` (disabled), `1` (enabled)
-- `ENABLE_SERVICE_MLFLOW`: Enable MLflow tracking (`0`/`1`)
-- `ENABLE_SERVICE_GLANCES`: Enable resource monitor (`0`/`1`)
-- `ENABLE_SERVICE_TENSORBOARD`: Enable TensorBoard (`0`/`1`)
-- `NVIDIA_AUTODETECT_IMAGE`: Image for GPU detection (default: `nvidia/cuda:12.9.1-base-ubuntu24.04`)
+- `JUPYTERHUB_GPU_ENABLED`: GPU mode - `0` (disabled), `1` (enabled), `2` (auto-detect)
+- `JUPYTERHUB_SSL_ENABLED`: Direct SSL config - `0` (disabled), `1` (enabled)
+- `JUPYTERHUB_SERVICE_MLFLOW`: Enable MLflow tracking (`0`/`1`)
+- `JUPYTERHUB_SERVICE_GLANCES`: Enable resource monitor (`0`/`1`)
+- `JUPYTERHUB_SERVICE_TENSORBOARD`: Enable TensorBoard (`0`/`1`)
+- `JUPYTERHUB_NVIDIA_IMAGE`: Image for GPU detection (default: `nvidia/cuda:12.9.1-base-ubuntu24.04`)
-**GPU Auto-Detection**: When `ENABLE_GPU_SUPPORT=2`, the platform attempts to run `nvidia-smi` in a CUDA container. If successful, GPU support is enabled for all spawned user containers via `device_requests`.
+**GPU Auto-Detection**: When `JUPYTERHUB_GPU_ENABLED=2`, the platform attempts to run `nvidia-smi` in a CUDA container. If successful, GPU support is enabled for all spawned user containers via `device_requests`.
**User Container Configuration**:
- Spawned containers use `DockerSpawner` with per-user volumes
@@ -96,7 +96,7 @@ services:
volumes:
- ./config/jupyterhub_config_override.py:/srv/jupyterhub/jupyterhub_config.py:ro
environment:
- - ENABLE_GPU_SUPPORT=1
+ - JUPYTERHUB_GPU_ENABLED=1
```
**IMPORTANT**: `compose_override.yml` contains deployment-specific credentials (CIFS passwords, etc.) and should never be committed.
@@ -300,8 +300,8 @@ User renames via JupyterHub admin panel automatically sync to NativeAuthenticato
**GPU not detected**:
- Verify NVIDIA Docker runtime: `docker run --rm --gpus all nvidia/cuda:12.9.1-base-ubuntu24.04 nvidia-smi`
-- Check `NVIDIA_AUTODETECT_IMAGE` matches your CUDA version
-- Manually enable with `ENABLE_GPU_SUPPORT=1`
+- Check `JUPYTERHUB_NVIDIA_IMAGE` matches your CUDA version
+- Manually enable with `JUPYTERHUB_GPU_ENABLED=1`
**Container spawn failures**:
- Check Docker socket permissions: `/var/run/docker.sock` must be accessible
diff --git a/.claude/JOURNAL.md b/.claude/JOURNAL.md
index 503f517..93f2739 100644
--- a/.claude/JOURNAL.md
+++ b/.claude/JOURNAL.md
@@ -141,3 +141,6 @@ This journal tracks substantive work on documents, diagrams, and documentation c
46. **Task - Idle server culler**: Implemented automatic shutdown of inactive servers
**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
+
+47. **Task - Standardize env vars with JUPYTERHUB_ prefix**: Renamed all environment variables to use consistent JUPYTERHUB_ prefix and added admin Settings page
+ **Result**: Renamed 13 environment variables (ENABLE_GPU_SUPPORT->JUPYTERHUB_GPU_ENABLED, ENABLE_JUPYTERHUB_SSL->JUPYTERHUB_SSL_ENABLED, ENABLE_SERVICE_*->JUPYTERHUB_SERVICE_*, ENABLE_SIGNUP->JUPYTERHUB_SIGNUP_ENABLED, DOCKER_NOTEBOOK_IMAGE->JUPYTERHUB_NOTEBOOK_IMAGE, DOCKER_NETWORK_NAME->JUPYTERHUB_NETWORK_NAME, NVIDIA_AUTODETECT_IMAGE->JUPYTERHUB_NVIDIA_IMAGE, IDLE_CULLER_*->JUPYTERHUB_IDLE_CULLER_*). Created SettingsPageHandler with admin-only access at /settings showing read-only banded table of all configuration values. Updated compose.yml, jupyterhub_config.py, custom_handlers.py, README.md, CLAUDE.md. Added Settings link to admin navbar. No backward compatibility for old names. Bumped version to 3.6.1
diff --git a/README.md b/README.md
index 497c350..de1c216 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Multi-user JupyterHub 4 deployment platform with data science stack, GPU support
- **User Self-Service**: Users can restart their JupyterLab containers and selectively reset persistent volumes (home/workspace/cache) without admin intervention
- **Docker Access Control**: Group-based access via `docker-sock` (container orchestration) and `docker-privileged` (full container privileges)
- **Isolated Environments**: Each user gets dedicated JupyterLab container with persistent volumes via DockerSpawner
-- **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
+- **Native Authentication**: Built-in user management with NativeAuthenticator supporting optional self-registration (`JUPYTERHUB_SIGNUP_ENABLED`) 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
@@ -95,19 +95,19 @@ graph TB
subgraph ENV["Environment Variables (compose.yml)"]
ADMIN[JUPYTERHUB_ADMIN
Admin username]
BASEURL[JUPYTERHUB_BASE_URL
URL prefix]
- IMG[DOCKER_NOTEBOOK_IMAGE
User container image]
- NET[DOCKER_NETWORK_NAME
Container network]
- SSL[ENABLE_JUPYTERHUB_SSL
0=off, 1=on]
- GPU[ENABLE_GPU_SUPPORT
0=off, 1=on, 2=auto]
- SIGNUP[ENABLE_SIGNUP
0=admin-only, 1=self-register]
+ IMG[JUPYTERHUB_NOTEBOOK_IMAGE
User container image]
+ NET[JUPYTERHUB_NETWORK_NAME
Container network]
+ SSL[JUPYTERHUB_SSL_ENABLED
0=off, 1=on]
+ GPU[JUPYTERHUB_GPU_ENABLED
0=off, 1=on, 2=auto]
+ SIGNUP[JUPYTERHUB_SIGNUP_ENABLED
0=admin-only, 1=self-register]
TFLOG[TF_CPP_MIN_LOG_LEVEL
TensorFlow verbosity]
- NVIMG[NVIDIA_AUTODETECT_IMAGE
CUDA test image]
+ NVIMG[JUPYTERHUB_NVIDIA_IMAGE
CUDA test image]
- subgraph SVCEN["ENABLE_SERVICE_*
Passed to Lab as env"]
+ subgraph SVCEN["JUPYTERHUB_SERVICE_*
Passed to Lab as env"]
direction LR
- MLF[ENABLE_SERVICE_MLFLOW]
- GLN[ENABLE_SERVICE_GLANCES]
- TNS[ENABLE_SERVICE_TENSORBOARD]
+ MLF[JUPYTERHUB_SERVICE_MLFLOW]
+ GLN[JUPYTERHUB_SERVICE_GLANCES]
+ TNS[JUPYTERHUB_SERVICE_TENSORBOARD]
SVC_MORE[...]
end
end
@@ -170,7 +170,7 @@ Environment variables defined in `compose.yml` are consumed by `config/jupyterhu
```mermaid
graph LR
- START[ENABLE_GPU_SUPPORT=2] --> CHECK{Check value}
+ START[JUPYTERHUB_GPU_ENABLED=2] --> CHECK{Check value}
CHECK -->|0| DISABLED[GPU Disabled]
CHECK -->|1| ENABLED[GPU Enabled]
CHECK -->|2| DETECT[Auto-detect]
@@ -179,8 +179,8 @@ graph LR
SPAWN --> RUN[Execute nvidia-smi
with runtime=nvidia]
RUN --> SUCCESS{Success?}
- SUCCESS -->|Yes| SET_ON[Set ENABLE_GPU_SUPPORT=1
Set NVIDIA_DETECTED=1]
- SUCCESS -->|No| SET_OFF[Set ENABLE_GPU_SUPPORT=0
Set NVIDIA_DETECTED=0]
+ SUCCESS -->|Yes| SET_ON[Set JUPYTERHUB_GPU_ENABLED=1
Set NVIDIA_DETECTED=1]
+ SUCCESS -->|No| SET_OFF[Set JUPYTERHUB_GPU_ENABLED=0
Set NVIDIA_DETECTED=0]
SET_ON --> CLEANUP1[Remove test container
jupyterhub_nvidia_autodetect]
SET_OFF --> CLEANUP2[Remove test container
jupyterhub_nvidia_autodetect]
@@ -199,7 +199,7 @@ graph LR
style APPLY_OFF stroke:#6b7280,stroke-width:2px
```
-When `ENABLE_GPU_SUPPORT=2` (auto-detect mode), JupyterHub spawns a temporary CUDA container running `nvidia-smi` with `runtime=nvidia`. If the command succeeds, GPU support is enabled and `device_requests` are added to spawned user containers. If it fails, GPU support is disabled. The test container is always removed after detection. Manual override is possible by setting `ENABLE_GPU_SUPPORT=1` (force enable) or `ENABLE_GPU_SUPPORT=0` (force disable).
+When `JUPYTERHUB_GPU_ENABLED=2` (auto-detect mode), JupyterHub spawns a temporary CUDA container running `nvidia-smi` with `runtime=nvidia`. If the command succeeds, GPU support is enabled and `device_requests` are added to spawned user containers. If it fails, GPU support is disabled. The test container is always removed after detection. Manual override is possible by setting `JUPYTERHUB_GPU_ENABLED=1` (force enable) or `JUPYTERHUB_GPU_ENABLED=0` (force disable).
## User Self-Service Workflow
@@ -340,14 +340,14 @@ services:
#### Enable GPU
No changes required in the configuration if you allow NVidia autodetection to be performed.
-Otherwise change the `ENABLE_GPU_SUPPORT = 1`
+Otherwise change the `JUPYTERHUB_GPU_ENABLED=1`
Changes in your `compose_override.yml`:
```yaml
services:
jupyterhub:
environment:
- - ENABLE_GPU_SUPPORT=1 # enable NVIDIA GPU, values: 0 - disabled, 1 - enabled, 2 - auto-detect
+ - JUPYTERHUB_GPU_ENABLED=1 # enable NVIDIA GPU, values: 0 - disabled, 1 - enabled, 2 - auto-detect
```
#### Disable self-registration
@@ -358,7 +358,7 @@ By default, users can self-register and require admin approval. To disable self-
services:
jupyterhub:
environment:
- - ENABLE_SIGNUP=0 # disable self-registration, admin creates users
+ - JUPYTERHUB_SIGNUP_ENABLED=0 # disable self-registration, admin creates users
```
#### Idle Server Culler
@@ -369,15 +369,15 @@ Automatically stop user servers after a period of inactivity to free up resource
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)
+ - JUPYTERHUB_IDLE_CULLER_ENABLED=1 # enable idle culler
+ - JUPYTERHUB_IDLE_CULLER_TIMEOUT=86400 # 24 hours (default) - stop after this many seconds of inactivity
+ - JUPYTERHUB_IDLE_CULLER_INTERVAL=600 # 10 minutes (default) - how often to check for idle servers
+ - JUPYTERHUB_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
+- `JUPYTERHUB_IDLE_CULLER_TIMEOUT`: Server is stopped after this many seconds without activity. Active servers are never culled
+- `JUPYTERHUB_IDLE_CULLER_MAX_AGE`: Force stop servers older than this (useful to force image updates). Set to 0 to disable
#### Custom Branding
diff --git a/compose.yml b/compose.yml
index bda459d..e82e840 100644
--- a/compose.yml
+++ b/compose.yml
@@ -59,20 +59,25 @@ services:
- jupyterhub_shared:/mnt/shared # shared volume across environments
- /var/run/docker.sock:/var/run/docker.sock:ro # for nvidia autodetection
environment:
+ # Core settings
- JUPYTERHUB_ADMIN=admin # this username will be a JupyterHub admin
- - DOCKER_NOTEBOOK_IMAGE=stellars/stellars-jupyterlab-ds:latest # jupyterlab image to spawn
- - DOCKER_NETWORK_NAME=jupyterhub_network # spawned containers will join this network
- - JUPYTERHUB_BASE_URL=/jupyterhub # default prefix
- - CERTIFICATE_DOMAIN_NAME=localhost # domain name for self-signed certificate generation
- - ENABLE_GPU_SUPPORT=2 # gpu status: 0 - disabled, 1 - enabled, 2 - auto-detect
- - ENABLE_JUPYTERHUB_SSL=0 # if using traefik - you do need direct SSL config
- - ENABLE_SERVICE_MLFLOW=1 # enable mlflow for experiment tracking
- - ENABLE_SERVICE_GLANCES=1 # enable resources monitor
- - ENABLE_SERVICE_TENSORBOARD=1 # enable tensorflow training tracker
- - ENABLE_SIGNUP=1 # user self-registration: 0 - disabled (admin creates users), 1 - enabled
- - TF_CPP_MIN_LOG_LEVEL=3 # make tensorflow less verbose
- - NVIDIA_AUTODETECT_IMAGE=nvidia/cuda:12.9.1-base-ubuntu24.04 # image with `nvidia-smi` for gpu autodetection
- - JUPYTERHUB_CUSTOM_LOGO_URI= # custom logo URL, file:/// path, or data URI (empty = stock logo)
+ - JUPYTERHUB_BASE_URL=/jupyterhub # default URL prefix
+ - JUPYTERHUB_SIGNUP_ENABLED=1 # user self-registration: 0=disabled, 1=enabled
+ - JUPYTERHUB_SSL_ENABLED=0 # direct SSL: 0=disabled (use Traefik), 1=enabled
+ # Docker spawner
+ - JUPYTERHUB_NOTEBOOK_IMAGE=stellars/stellars-jupyterlab-ds:latest # user container image
+ - JUPYTERHUB_NETWORK_NAME=jupyterhub_network # container network
+ # GPU support
+ - JUPYTERHUB_GPU_ENABLED=2 # 0=disabled, 1=enabled, 2=auto-detect
+ - JUPYTERHUB_NVIDIA_IMAGE=nvidia/cuda:13.0.2-base-ubuntu24.04 # GPU detection image
+ # User environment services
+ - JUPYTERHUB_SERVICE_MLFLOW=1 # MLflow experiment tracking
+ - JUPYTERHUB_SERVICE_GLANCES=1 # system resources monitor
+ - JUPYTERHUB_SERVICE_TENSORBOARD=1 # TensorFlow training tracker
+ # Misc
+ - TF_CPP_MIN_LOG_LEVEL=3 # TensorFlow verbosity
+ - CERTIFICATE_DOMAIN_NAME=localhost # self-signed certificate domain
+ - JUPYTERHUB_CUSTOM_LOGO_URI= # custom logo (empty = stock logo)
labels:
# Enable proxy support from Traefik
- "traefik.enable=true"
diff --git a/config/jupyterhub_config.py b/config/jupyterhub_config.py
index 524a6d3..4b05dc0 100644
--- a/config/jupyterhub_config.py
+++ b/config/jupyterhub_config.py
@@ -182,40 +182,42 @@ def detect_nvidia(nvidia_autodetect_image='nvidia/cuda:12.9.1-base-ubuntu24.04')
return result
-# standard variables imported from env
-ENABLE_JUPYTERHUB_SSL = int(os.environ.get("ENABLE_JUPYTERHUB_SSL", 1))
-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)
-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))
+# Standard variables imported from env (all use JUPYTERHUB_ prefix)
+JUPYTERHUB_SSL_ENABLED = int(os.environ.get("JUPYTERHUB_SSL_ENABLED", 1))
+JUPYTERHUB_GPU_ENABLED = int(os.environ.get("JUPYTERHUB_GPU_ENABLED", 2))
+JUPYTERHUB_SERVICE_MLFLOW = int(os.environ.get("JUPYTERHUB_SERVICE_MLFLOW", 1))
+JUPYTERHUB_SERVICE_GLANCES = int(os.environ.get("JUPYTERHUB_SERVICE_GLANCES", 1))
+JUPYTERHUB_SERVICE_TENSORBOARD = int(os.environ.get("JUPYTERHUB_SERVICE_TENSORBOARD", 1))
+JUPYTERHUB_SIGNUP_ENABLED = int(os.environ.get("JUPYTERHUB_SIGNUP_ENABLED", 1)) # 0=disabled, 1=enabled
+JUPYTERHUB_IDLE_CULLER_ENABLED = int(os.environ.get("JUPYTERHUB_IDLE_CULLER_ENABLED", 0))
+JUPYTERHUB_IDLE_CULLER_TIMEOUT = int(os.environ.get("JUPYTERHUB_IDLE_CULLER_TIMEOUT", 86400))
+JUPYTERHUB_IDLE_CULLER_INTERVAL = int(os.environ.get("JUPYTERHUB_IDLE_CULLER_INTERVAL", 600))
+JUPYTERHUB_IDLE_CULLER_MAX_AGE = int(os.environ.get("JUPYTERHUB_IDLE_CULLER_MAX_AGE", 0))
+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")
+JUPYTERHUB_NETWORK_NAME = os.environ.get("JUPYTERHUB_NETWORK_NAME", "jupyterhub_network")
+JUPYTERHUB_NOTEBOOK_IMAGE = os.environ.get("JUPYTERHUB_NOTEBOOK_IMAGE", "stellars/stellars-jupyterlab-ds:latest")
+JUPYTERHUB_NVIDIA_IMAGE = os.environ.get("JUPYTERHUB_NVIDIA_IMAGE", "nvidia/cuda:12.9.1-base-ubuntu24.04")
# Normalize base URL - use empty string for root path to avoid double slashes
if JUPYTERHUB_BASE_URL in ['/', '', None]:
JUPYTERHUB_BASE_URL_PREFIX = ''
else:
JUPYTERHUB_BASE_URL_PREFIX = JUPYTERHUB_BASE_URL
-JUPYTERHUB_ADMIN = os.environ.get("JUPYTERHUB_ADMIN")
-NETWORK_NAME = os.environ["DOCKER_NETWORK_NAME"]
-NVIDIA_AUTODETECT_IMAGE = os.environ.get("NVIDIA_AUTODETECT_IMAGE", 'nvidia/cuda:12.9.1-base-ubuntu24.04')
+JUPYTERHUB_ADMIN = os.environ.get("JUPYTERHUB_ADMIN")
-# perform autodetection when ENABLE_GPU_SUPPORT is set to autodetect
+# perform autodetection when JUPYTERHUB_GPU_ENABLED is set to autodetect
# gpu support: 0 - disabled, 1 - enabled, 2 - autodetect
-if ENABLE_GPU_SUPPORT == 2:
- NVIDIA_DETECTED = detect_nvidia(NVIDIA_AUTODETECT_IMAGE)
- if NVIDIA_DETECTED: ENABLE_GPU_SUPPORT = 1 # means - gpu enabled
- else: ENABLE_GPU_SUPPORT = 0 # means - disable
+NVIDIA_DETECTED = 0 # Initialize before potential auto-detection
+if JUPYTERHUB_GPU_ENABLED == 2:
+ NVIDIA_DETECTED = detect_nvidia(JUPYTERHUB_NVIDIA_IMAGE)
+ if NVIDIA_DETECTED: JUPYTERHUB_GPU_ENABLED = 1 # means - gpu enabled
+ else: JUPYTERHUB_GPU_ENABLED = 0 # means - disable
# Apply JupyterHub configuration (only when loaded by JupyterHub, not when imported)
if c is not None:
# ensure that we are using SSL, it should be enabled by default
- if ENABLE_JUPYTERHUB_SSL == 1:
+ if JUPYTERHUB_SSL_ENABLED == 1:
c.JupyterHub.ssl_cert = '/mnt/certs/server.crt'
c.JupyterHub.ssl_key = '/mnt/certs/server.key'
@@ -230,16 +232,16 @@ if c is not None:
'MLFLOW_PORT':5000,
'MLFLOW_HOST':'0.0.0.0', # new 3.5 mlflow launched with guinicorn requires this
'MLFLOW_WORKERS':1,
- 'ENABLE_SERVICE_MLFLOW': ENABLE_SERVICE_MLFLOW,
- 'ENABLE_SERVICE_GLANCES': ENABLE_SERVICE_GLANCES,
- 'ENABLE_SERVICE_TENSORBOARD': ENABLE_SERVICE_TENSORBOARD,
- 'ENABLE_GPU_SUPPORT': ENABLE_GPU_SUPPORT,
- 'ENABLE_GPUSTAT': ENABLE_GPU_SUPPORT,
+ 'JUPYTERHUB_SERVICE_MLFLOW': JUPYTERHUB_SERVICE_MLFLOW,
+ 'JUPYTERHUB_SERVICE_GLANCES': JUPYTERHUB_SERVICE_GLANCES,
+ 'JUPYTERHUB_SERVICE_TENSORBOARD': JUPYTERHUB_SERVICE_TENSORBOARD,
+ 'JUPYTERHUB_GPU_ENABLED': JUPYTERHUB_GPU_ENABLED,
+ 'ENABLE_GPUSTAT': JUPYTERHUB_GPU_ENABLED,
'NVIDIA_DETECTED': NVIDIA_DETECTED,
}
# configure access to GPU if possible
- if ENABLE_GPU_SUPPORT == 1:
+ if JUPYTERHUB_GPU_ENABLED == 1:
c.DockerSpawner.extra_host_config = {
'device_requests': [
{
@@ -251,11 +253,11 @@ if c is not None:
}
# spawn containers from this image
- c.DockerSpawner.image = os.environ["DOCKER_NOTEBOOK_IMAGE"]
+ c.DockerSpawner.image = JUPYTERHUB_NOTEBOOK_IMAGE
- # networking congfiguration
+ # networking configuration
c.DockerSpawner.use_internal_ip = True
- c.DockerSpawner.network_name = NETWORK_NAME
+ c.DockerSpawner.network_name = JUPYTERHUB_NETWORK_NAME
# prevent auto-spawn for admin users
# Redirect admin to admin panel instead
@@ -394,7 +396,7 @@ if c is not None:
# allow anyone to sign-up without approval
# allow all signed-up users to login
c.NativeAuthenticator.open_signup = False
- c.NativeAuthenticator.enable_signup = bool(ENABLE_SIGNUP) # controlled by ENABLE_SIGNUP env var
+ c.NativeAuthenticator.enable_signup = bool(JUPYTERHUB_SIGNUP_ENABLED) # controlled by JUPYTERHUB_SIGNUP_ENABLED env var
c.Authenticator.allow_all = True
# allowed admins
@@ -413,7 +415,8 @@ if c is not None:
NotificationsPageHandler,
ActiveServersHandler,
BroadcastNotificationHandler,
- GetUserCredentialsHandler
+ GetUserCredentialsHandler,
+ SettingsPageHandler
)
c.JupyterHub.extra_handlers = [
@@ -423,10 +426,11 @@ if c is not None:
(r'/api/notifications/broadcast', BroadcastNotificationHandler),
(r'/api/admin/credentials', GetUserCredentialsHandler),
(r'/notifications', NotificationsPageHandler),
+ (r'/settings', SettingsPageHandler),
]
# Idle culler service - automatically stops servers after inactivity
- if IDLE_CULLER_ENABLED == 1:
+ if JUPYTERHUB_IDLE_CULLER_ENABLED == 1:
import sys
# Define role with required scopes for idle culler
@@ -447,11 +451,11 @@ if c is not None:
culler_cmd = [
sys.executable,
"-m", "jupyterhub_idle_culler",
- f"--timeout={IDLE_CULLER_TIMEOUT}",
- f"--cull-every={IDLE_CULLER_CULL_EVERY}",
+ f"--timeout={JUPYTERHUB_IDLE_CULLER_TIMEOUT}",
+ f"--cull-every={JUPYTERHUB_IDLE_CULLER_INTERVAL}",
]
- if IDLE_CULLER_MAX_AGE > 0:
- culler_cmd.append(f"--max-age={IDLE_CULLER_MAX_AGE}")
+ if JUPYTERHUB_IDLE_CULLER_MAX_AGE > 0:
+ culler_cmd.append(f"--max-age={JUPYTERHUB_IDLE_CULLER_MAX_AGE}")
c.JupyterHub.services = [
{
@@ -460,6 +464,6 @@ if c is not None:
}
]
- print(f"[Idle Culler] Enabled - timeout={IDLE_CULLER_TIMEOUT}s, check every={IDLE_CULLER_CULL_EVERY}s, max_age={IDLE_CULLER_MAX_AGE}s")
+ print(f"[Idle Culler] Enabled - timeout={JUPYTERHUB_IDLE_CULLER_TIMEOUT}s, check every={JUPYTERHUB_IDLE_CULLER_INTERVAL}s, max_age={JUPYTERHUB_IDLE_CULLER_MAX_AGE}s")
# EOF
diff --git a/project.env b/project.env
index 9846073..ec1100c 100644
--- a/project.env
+++ b/project.env
@@ -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.6.0_cuda-12.9.1_jh-5.4.2"
-VERSION_COMMENT="Idle server culler, selective notifications, volume encoding fix"
+VERSION="3.6.3_cuda-13.0.2_jh-5.4.2"
+VERSION_COMMENT="Standardize env vars with JUPYTERHUB_ prefix, admin settings page"
RELEASE_TAG="RELEASE_3.2.11"
RELEASE_DATE="2025-11-09"
diff --git a/services/jupyterhub/conf/bin/custom_handlers.py b/services/jupyterhub/conf/bin/custom_handlers.py
index bc29ab4..f5de323 100755
--- a/services/jupyterhub/conf/bin/custom_handlers.py
+++ b/services/jupyterhub/conf/bin/custom_handlers.py
@@ -10,6 +10,7 @@ import docker
import json
import asyncio
import time
+import os
# =============================================================================
@@ -560,3 +561,58 @@ class GetUserCredentialsHandler(BaseHandler):
self.log.info(f"[Get Credentials] Returning {len(credentials)} credential(s)")
self.finish({"credentials": credentials})
+
+
+class SettingsPageHandler(BaseHandler):
+ """Handler for rendering the settings page (admin only, read-only)"""
+
+ @web.authenticated
+ async def get(self):
+ """
+ Render the settings page showing key environment variables
+
+ GET /settings
+ """
+ current_user = self.current_user
+
+ # Only admins can access settings
+ if not current_user.admin:
+ raise web.HTTPError(403, "Only administrators can access this page")
+
+ self.log.info(f"[Settings Page] Admin {current_user.name} accessed settings panel")
+
+ # Collect environment variables and settings
+ settings = []
+
+ # JupyterHub Core Settings
+ settings.append({"category": "JupyterHub Core", "name": "JUPYTERHUB_ADMIN", "value": os.environ.get("JUPYTERHUB_ADMIN", "admin"), "description": "Admin username"})
+ settings.append({"category": "JupyterHub Core", "name": "JUPYTERHUB_BASE_URL", "value": os.environ.get("JUPYTERHUB_BASE_URL", "/jupyterhub"), "description": "Base URL path"})
+ settings.append({"category": "JupyterHub Core", "name": "JUPYTERHUB_SIGNUP_ENABLED", "value": os.environ.get("JUPYTERHUB_SIGNUP_ENABLED", "1"), "description": "User self-registration (0=disabled, 1=enabled)"})
+ settings.append({"category": "JupyterHub Core", "name": "JUPYTERHUB_SSL_ENABLED", "value": os.environ.get("JUPYTERHUB_SSL_ENABLED", "1"), "description": "SSL/TLS (0=disabled, 1=enabled)"})
+
+ # Docker/Spawner Settings
+ settings.append({"category": "Docker Spawner", "name": "JUPYTERHUB_NOTEBOOK_IMAGE", "value": os.environ.get("JUPYTERHUB_NOTEBOOK_IMAGE", "stellars/stellars-jupyterlab-ds:latest"), "description": "User container image"})
+ settings.append({"category": "Docker Spawner", "name": "JUPYTERHUB_NETWORK_NAME", "value": os.environ.get("JUPYTERHUB_NETWORK_NAME", "jupyterhub_network"), "description": "Docker network for containers"})
+
+ # GPU Settings
+ settings.append({"category": "GPU", "name": "JUPYTERHUB_GPU_ENABLED", "value": os.environ.get("JUPYTERHUB_GPU_ENABLED", "2"), "description": "GPU support (0=disabled, 1=enabled, 2=auto-detect)"})
+ settings.append({"category": "GPU", "name": "JUPYTERHUB_NVIDIA_IMAGE", "value": os.environ.get("JUPYTERHUB_NVIDIA_IMAGE", "nvidia/cuda:12.9.1-base-ubuntu24.04"), "description": "CUDA image for GPU detection"})
+
+ # Services
+ settings.append({"category": "Services", "name": "JUPYTERHUB_SERVICE_MLFLOW", "value": os.environ.get("JUPYTERHUB_SERVICE_MLFLOW", "1"), "description": "MLflow service (0=disabled, 1=enabled)"})
+ settings.append({"category": "Services", "name": "JUPYTERHUB_SERVICE_GLANCES", "value": os.environ.get("JUPYTERHUB_SERVICE_GLANCES", "1"), "description": "Glances service (0=disabled, 1=enabled)"})
+ settings.append({"category": "Services", "name": "JUPYTERHUB_SERVICE_TENSORBOARD", "value": os.environ.get("JUPYTERHUB_SERVICE_TENSORBOARD", "1"), "description": "TensorBoard service (0=disabled, 1=enabled)"})
+
+ # Idle Culler
+ settings.append({"category": "Idle Culler", "name": "JUPYTERHUB_IDLE_CULLER_ENABLED", "value": os.environ.get("JUPYTERHUB_IDLE_CULLER_ENABLED", "0"), "description": "Idle culler (0=disabled, 1=enabled)"})
+ settings.append({"category": "Idle Culler", "name": "JUPYTERHUB_IDLE_CULLER_TIMEOUT", "value": os.environ.get("JUPYTERHUB_IDLE_CULLER_TIMEOUT", "86400"), "description": "Idle timeout in seconds"})
+ settings.append({"category": "Idle Culler", "name": "JUPYTERHUB_IDLE_CULLER_INTERVAL", "value": os.environ.get("JUPYTERHUB_IDLE_CULLER_INTERVAL", "600"), "description": "Check interval in seconds"})
+ settings.append({"category": "Idle Culler", "name": "JUPYTERHUB_IDLE_CULLER_MAX_AGE", "value": os.environ.get("JUPYTERHUB_IDLE_CULLER_MAX_AGE", "0"), "description": "Max server age (0=unlimited)"})
+
+ # Branding
+ settings.append({"category": "Branding", "name": "JUPYTERHUB_CUSTOM_LOGO_URI", "value": os.environ.get("JUPYTERHUB_CUSTOM_LOGO_URI", "") or "(default)", "description": "Custom logo URI"})
+ settings.append({"category": "Branding", "name": "STELLARS_JUPYTERHUB_VERSION", "value": os.environ.get("STELLARS_JUPYTERHUB_VERSION", "unknown"), "description": "Platform version"})
+
+ # Render the template
+ html = self.render_template("settings.html", sync=True, user=current_user, settings=settings)
+ self.finish(html)
diff --git a/services/jupyterhub/templates/404.html b/services/jupyterhub/templates/404.html
new file mode 100644
index 0000000..04c7494
--- /dev/null
+++ b/services/jupyterhub/templates/404.html
@@ -0,0 +1,4 @@
+{% extends "error.html" %}
+{% block error_detail %}
+
Jupyter has lots of moons, but this is not one...
+{% endblock error_detail %} diff --git a/services/jupyterhub/templates/accept-share.html b/services/jupyterhub/templates/accept-share.html new file mode 100644 index 0000000..450795e --- /dev/null +++ b/services/jupyterhub/templates/accept-share.html @@ -0,0 +1,52 @@ +{% extends "page.html" %} +{% block login_widget %} +{% endblock login_widget %} +{% block main %} ++ You ({{ user.name }}) have been invited to access {{ owner.name }}'s server + {%- if spawner.name %}({{ spawner.name }}){%- endif %} at {{ spawner_url }} +
+ {% if not spawner_ready %} ++ The server at {{ spawner_url }} is not currently running. + After accepting permission, you may need to ask {{ owner.name }} + to start the server before you can access it. +
+ {% endif %} + +{{ message_html | safe }}
+ {% elif message %} +{{ message }}
+ {% endif %} + {% if extra_error_html %}{{ extra_error_html | safe }}
{% endif %} + {% endblock error_detail %} +Successfully logged out.
++ {% if failed %} + The latest attempt to start your server {{ server_name }} has failed. + {% if failed_html_message %} +
+{{ failed_html_message | safe }}
+{% elif failed_message %}
+{{ failed_message }}
++ {% endif %} + Would you like to retry starting it? + {% else %} + Your server {{ server_name }} is not running. + {% if implicit_spawn_seconds %} + It will be restarted automatically. + If you are not redirected in a few seconds, + click below to launch your server. + {% else %} + Would you like to start it? + {% endif %} + {% endif %} +
+ {% endblock message %} + {% block start_button %} + + {% if failed %} + Relaunch + {% else %} + Launch + {% endif %} + Server {{ server_name }} + + {% endblock start_button %} +An application is requesting authorization to access data associated with your JupyterHub account
++ {{ oauth_client.description }} (oauth URL: {{ oauth_client.redirect_uri }}) + would like permission to identify you. + {% if scope_descriptions | length == 1 and not scope_descriptions[0].scope %} + It will not be able to take actions on + your behalf. + {% endif %} +
+ +Read-only view of current environment variables and configuration
+ +| Category | +Setting | +Value | +Description | +
|---|---|---|---|
| + {% if setting.category != current_category.value %} + {{ setting.category }} + {% set current_category.value = setting.category %} + {% endif %} + | +{{ setting.name }} |
+ + {% if setting.value|length > 50 %} + {{ setting.value[:47] }}... + {% else %} + {{ setting.value }} + {% endif %} + | +{{ setting.description }} | +
+ These settings are configured via environment variables in compose.yml or compose_override.yml.
+ Changes require container restart to take effect.
+
Spawning server for {{ for_user.name }}
+ {% endif -%} + {% if error_message %} +Error: {{ error_message }}
+ {% elif error_html_message %} +{{ error_html_message | safe }}
+ {% endif %} + +Your server is starting up.
+You will be redirected automatically when it's ready for you.
+ {% endblock message %} +Your server is stopping.
+You will be able to start it again once it has finished stopping.
+ {% endblock message %} ++ +
+ refresh ++ These are tokens with access to the JupyterHub API. + Permissions for each token may be viewed via the JupyterHub tokens API. + Revoking the API token for a running server will require restarting that server. +
+| Note | +Permissions | +Last used | +Created | +Expires | +|
|---|---|---|---|---|---|
| {{ token.note }} | +
+
+
+ scopes+ {% for scope in token.scopes %}{{ scope }}{% endfor %}
+ |
+ + {%- if token.last_activity -%} + {{ token.last_activity.isoformat() + 'Z' }} + {%- else -%} + Never + {%- endif -%} + | ++ {%- if token.created -%} + {{ token.created.isoformat() + 'Z' }} + {%- else -%} + N/A + {%- endif -%} + | ++ {%- if token.expires_at -%} + {{ token.expires_at.isoformat() + 'Z' }} + {%- else -%} + Never + {%- endif -%} + | ++ + | + {% endblock token_row %} +
+ These are applications that use OAuth with JupyterHub + to identify users (mostly notebook servers). + OAuth tokens can generally only be used to identify you, + not take actions on your behalf. +
+| Application | +Permissions | +Last used | +First authorized | +|
|---|---|---|---|---|
| {{ client['description'] }} | +
+
+
+ scopes+ {# create set of scopes on all tokens -#} + {# sum concatenates all token.scopes into a single list -#} + {# then filter to unique set and sort -#} + {% for scope in client.tokens | sum(attribute="scopes", start=[]) | unique | sort %} +{{ scope }}
+ {% endfor %}
+ |
+ + {%- if client['last_activity'] -%} + {{ client['last_activity'].isoformat() + 'Z' }} + {%- else -%} + Never + {%- endif -%} + | ++ {%- if client['created'] -%} + {{ client['created'].isoformat() + 'Z' }} + {%- else -%} + N/A + {%- endif -%} + | ++ + | + {% endblock client_row %} +
Read-only view of current environment variables and configuration
+ +| Category | +Setting | +Value | +Description | +
|---|---|---|---|
| + {% if setting.category != current_category.value %} + {{ setting.category }} + {% set current_category.value = setting.category %} + {% endif %} + | +{{ setting.name }} |
+ + {% if setting.value|length > 50 %} + {{ setting.value[:47] }}... + {% else %} + {{ setting.value }} + {% endif %} + | +{{ setting.description }} | +
+ These settings are configured via environment variables in compose.yml or compose_override.yml.
+ Changes require container restart to take effect.
+