JupyterHub Notebooks¶
ExaMLOps ships a JupyterHub environment so researchers from different institutions can run notebooks with direct access to the full production stack — MLflow, MinIO, Ray Serve, Prefect, and the Control Plane.
Each user who logs in gets a dedicated JupyterLab container spawned automatically on login. That container joins the examlops_default Docker network, so all internal service hostnames work without any manual configuration.
Starting JupyterHub¶
Open http://localhost:18888 (or http://lxp-cpu01, or use ssh lxp port-forward).
First-Time Login (Admin Account)¶
JupyterHub uses NativeAuthenticator — there is no pre-set password. On first use:
- Open http://localhost:18888
- Click Sign up and register with username
adminand any password you choose - Click Login — because
adminis inadmin_users, your account is auto-approved - You are now inside your personal JupyterLab
Note: With
open_signup = False, accounts created by non-admin users are not auto-approved. The admin must approve them via the admin panel (see User Management).
Pre-configured Environment Variables¶
Every user container automatically has these env vars set — no configuration needed:
| Variable | Value |
|---|---|
MLFLOW_TRACKING_URI |
http://mlflow:5000 |
MLFLOW_S3_ENDPOINT_URL |
http://minio:9000 |
AWS_ACCESS_KEY_ID |
(MinIO root user from stack config) |
AWS_SECRET_ACCESS_KEY |
(MinIO root password from stack config) |
PREFECT_API_URL |
http://orchestrator:4200/api |
RAY_SERVE_URL |
http://ray-serving:8001 |
CONTROL_PLANE_URL |
http://control-plane:8002 |
Verify them in a notebook cell:
import os
for var in [
"MLFLOW_TRACKING_URI", "MLFLOW_S3_ENDPOINT_URL",
"PREFECT_API_URL", "RAY_SERVE_URL", "CONTROL_PLANE_URL",
]:
print(f"{var} = {os.environ.get(var, 'NOT SET')}")
Connecting to Services¶
MLflow — Experiment Tracking¶
import mlflow
import os
# Tracking URI is already set — but mlflow reads it from the env automatically
# so this line is optional
mlflow.set_tracking_uri(os.environ["MLFLOW_TRACKING_URI"])
client = mlflow.tracking.MlflowClient()
# List all registered models
for rm in client.search_registered_models():
print(rm.name)
for v in rm.latest_versions:
print(f" version={v.version} stage={v.current_stage} aliases={v.aliases}")
# Search runs in a specific experiment
runs = client.search_runs(
experiment_ids=["1"], # find experiment IDs at http://mlflow:5000
order_by=["metrics.val_loss ASC"],
max_results=10,
)
for r in runs:
print(r.info.run_id, r.data.metrics)
# Log a new run from a notebook
with mlflow.start_run(experiment_id="1", run_name="notebook-test"):
mlflow.log_param("learning_rate", 0.01)
mlflow.log_metric("accuracy", 0.95)
print("Run logged:", mlflow.active_run().info.run_id)
MLflow UI is also reachable in the browser at http://localhost:15000 (host) or http://
MinIO — Artifact Storage¶
import boto3
import os
s3 = boto3.client(
"s3",
endpoint_url=os.environ["MLFLOW_S3_ENDPOINT_URL"],
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
)
# List buckets
for b in s3.list_buckets()["Buckets"]:
print(b["Name"])
# List objects in the MLflow artifacts bucket
for obj in s3.list_objects_v2(Bucket="mlflow-artifacts").get("Contents", []):
print(obj["Key"], f" {obj['Size']} bytes")
# Download a specific artifact
s3.download_file("mlflow-artifacts", "path/to/model.pkl", "/tmp/model.pkl")
MinIO Console (browser UI): http://localhost:19001 — login with minioadmin / minioadmin (default).
Ray Serve — Model Inference¶
import requests
import os
base = os.environ["RAY_SERVE_URL"]
# Health check
print(requests.get(f"{base}/health").json())
# List available models
print(requests.get(f"{base}/models").json())
# Single prediction (Production alias of JPCP model)
resp = requests.post(
f"{base}/predict/jpcp",
json={"features": {"feature_0": 1.2, "feature_1": 0.8, "feature_2": -0.3}},
)
print(resp.status_code, resp.json())
# Prediction against a specific alias
resp = requests.post(
f"{base}/predict/jpcp",
json={"features": {"feature_0": 1.2}},
params={"alias": "Canary"},
)
print(resp.json())
Ray Dashboard: http://localhost:18265 (local) / http://
Prefect — Pipeline Orchestration¶
from prefect.client.orchestration import get_client
import asyncio
async def list_flows():
async with get_client() as client:
flows = await client.read_flows()
for f in flows:
print(f.name, f.id)
asyncio.run(list_flows())
# Trigger a pipeline run programmatically
async def trigger_run(deployment_name: str):
async with get_client() as client:
deployments = await client.read_deployments()
dep = next(d for d in deployments if d.name == deployment_name)
flow_run = await client.create_flow_run_from_deployment(dep.id)
print("Flow run created:", flow_run.id)
asyncio.run(trigger_run("jpcp-nightly"))
Prefect UI: http://localhost:14200 (local) / http://
Control Plane — Trigger Retraining¶
import requests
import os
url = os.environ["CONTROL_PLANE_URL"]
# Trigger a retrain job (requires CONTROL_PLANE_TOKEN)
import os
token = os.environ.get("CONTROL_PLANE_TOKEN", "") # set this if using auth
resp = requests.post(
f"{url}/retrain",
json={"model": "JPCP", "dataset": "PM100Dataset", "dummy": True},
headers={"Authorization": f"Bearer {token}"} if token else {},
)
print(resp.status_code, resp.json())
# Check approval queue
resp = requests.get(f"{url}/approvals/pending")
print(resp.json())
File Persistence¶
| Location | Persists across restarts? | Notes |
|---|---|---|
/home/jovyan/work/ |
Yes — Docker volume jupyter-user-{username} |
Your notebooks and personal files |
/home/jovyan/ (outside work/) |
No — lost on container restart | Temporary installs, scratch files |
/tmp/ |
No | Truly ephemeral |
Rule: Keep all notebooks and data you care about inside ~/work/.
Installing Additional Packages¶
Packages installed at runtime survive only until your container is stopped:
For a permanent install, ask the admin to add the package to Dockerfile.jupyterlab and rebuild:
VS Code Connectivity¶
- Install the Jupyter extension in VS Code
- Command Palette (
Ctrl+Shift+P) → Jupyter: Specify Jupyter Server for Connections - Select Existing
- Enter:
http://<host>:18888/user/<your-username>/?token=<token>
Your token: log in → click your name (top right) → Token → Request new API token.
User Management (Admin)¶
Add a user¶
# Generate an admin token first at http://localhost:18888/hub/token
make jupyter-add-user USER=alice HUB_TOKEN=<admin-token>
Then set the new user's password via http://localhost:18888/hub/admin → find the user → Edit.
Remove a user¶
curl -X DELETE http://localhost:18888/hub/api/users/<username> \
-H "Authorization: token <admin-token>"
The user's home volume (jupyter-user-<username>) is not deleted automatically — run docker volume rm jupyter-user-<username> to free disk space.
Admin panel¶
http://localhost:18888/hub/admin — start, stop, and inspect any user's server.
Operations Reference¶
| Command | Purpose |
|---|---|
make jupyter-up |
Build images + start JupyterHub (port 18888) |
make jupyter-down |
Stop JupyterHub (user volumes preserved) |
make jupyter-logs |
Tail Hub container logs |
make jupyter-add-user USER=x HUB_TOKEN=y |
Add a user via REST API |
Troubleshooting¶
"Server failed to start" after login¶
Your per-user container failed to launch. Check Hub logs:
Common causes:
- examlops-jupyterlab image not built — run make jupyter-up again, it rebuilds the image
- Port conflict — another process on port 18888; check with ss -tlnp | grep 18888
- Docker socket permission — Hub needs /var/run/docker.sock; verify with docker ps
Services return connection errors from inside a notebook¶
The full stack must be running:
Without it, the internal hostnames (mlflow, minio, etc.) do not resolve even though the env vars are set.
Lost my token¶
Log in → click username (top right) → Token → revoke old tokens → Request new API token.
My notebooks disappeared¶
Files outside ~/work/ are lost on container restart. In future, save everything to ~/work/.
Ports Quick Reference¶
| Service | Local | Remote (lxp-cpu01) |
|---|---|---|
| JupyterHub | http://localhost:18888 | http:// |
| MLflow | http://localhost:15000 | http:// |
| MinIO Console | http://localhost:19001 | http:// |
| Prefect UI | http://localhost:14200 | http:// |
| Ray Dashboard | http://localhost:18265 | http:// |
| Grafana | http://localhost:13000 | http:// |