feat: SQL=postgres, nginx+certbot, MQTT split, ML stacks, gitea HTTPS-only, gemaal1 site
Round-2 changes locking in scaffold-phase decisions and adding ML/notebook stacks.
Locked decisions
- sql: postgres 16-alpine (was TBD); init.d/ mount for per-app DB provisioning
- nginx-proxy: stock nginx + certbot sidecar (was nginx:alpine TODO).
Chose stock over nginxproxy/nginx-proxy because stream{} is required for
MQTT-TLS reverse-proxy on tcp/8883 to rabbitmq:1883.
- gitea: HTTPS-only (DISABLE_SSH=true). No SSH port published.
MQTT split
- Remove stacks/mqtt placeholder.
- Add stacks/rabbitmq — general-purpose broker (AMQP + MQTT plugin),
used at both cloud and edge. External MQTT clients reach cloud broker
via nginx stream-proxy on 8883.
- Add stacks/mosquitto — reserved for the FROST (SensorThings) stack
only. Cloud-only. Internal to its own stack; no external ingress.
ML / notebooks (cloud-only)
- stacks/mlflow — experiment tracking + model registry. Postgres backend
on sql stack; local volume for artifacts (S3/MinIO is a TODO).
- stacks/jupyterhub — multi-user notebook server. DockerSpawner via
mounted docker.sock; users spawn into cloud-app network so they can
reach mlflow, influxdb (via grafana), rabbitmq.
Sites
- sites/gemaal1 — first edge deployment scaffold. Site-local override
template for binding nginx to PLANT_LAN_IP.
Docs
- README + docs/architecture.md updated: stacks table now lists 15 stacks,
ingress + attachment tables reflect mlflow/jupyterhub, TLS strategy
section locked, MQTT-split section added, Gitea HTTPS-only noted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:22:46 +02:00
# jupyterhub
2026-05-21 18:34:37 +00:00
Multi-user JupyterHub. **Cloud-only. **
feat: SQL=postgres, nginx+certbot, MQTT split, ML stacks, gitea HTTPS-only, gemaal1 site
Round-2 changes locking in scaffold-phase decisions and adding ML/notebook stacks.
Locked decisions
- sql: postgres 16-alpine (was TBD); init.d/ mount for per-app DB provisioning
- nginx-proxy: stock nginx + certbot sidecar (was nginx:alpine TODO).
Chose stock over nginxproxy/nginx-proxy because stream{} is required for
MQTT-TLS reverse-proxy on tcp/8883 to rabbitmq:1883.
- gitea: HTTPS-only (DISABLE_SSH=true). No SSH port published.
MQTT split
- Remove stacks/mqtt placeholder.
- Add stacks/rabbitmq — general-purpose broker (AMQP + MQTT plugin),
used at both cloud and edge. External MQTT clients reach cloud broker
via nginx stream-proxy on 8883.
- Add stacks/mosquitto — reserved for the FROST (SensorThings) stack
only. Cloud-only. Internal to its own stack; no external ingress.
ML / notebooks (cloud-only)
- stacks/mlflow — experiment tracking + model registry. Postgres backend
on sql stack; local volume for artifacts (S3/MinIO is a TODO).
- stacks/jupyterhub — multi-user notebook server. DockerSpawner via
mounted docker.sock; users spawn into cloud-app network so they can
reach mlflow, influxdb (via grafana), rabbitmq.
Sites
- sites/gemaal1 — first edge deployment scaffold. Site-local override
template for binding nginx to PLANT_LAN_IP.
Docs
- README + docs/architecture.md updated: stacks table now lists 15 stacks,
ingress + attachment tables reflect mlflow/jupyterhub, TLS strategy
section locked, MQTT-split section added, Gitea HTTPS-only noted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:22:46 +02:00
2026-05-21 18:34:37 +00:00
- **Hostname**: `hub.wbd-rd.nl`
- **Networks**: `app` (UI proxied) + `mgmt` (Docker socket — for DockerSpawner once we switch to it)
- **Config**: `config/jupyterhub_config.py`
- **Image**: built locally (`cloud-jupyterhub:5` ) — upstream JupyterHub + `oauthenticator` + `jupyterlab` + `notebook` . See `Dockerfile` .
feat: SQL=postgres, nginx+certbot, MQTT split, ML stacks, gitea HTTPS-only, gemaal1 site
Round-2 changes locking in scaffold-phase decisions and adding ML/notebook stacks.
Locked decisions
- sql: postgres 16-alpine (was TBD); init.d/ mount for per-app DB provisioning
- nginx-proxy: stock nginx + certbot sidecar (was nginx:alpine TODO).
Chose stock over nginxproxy/nginx-proxy because stream{} is required for
MQTT-TLS reverse-proxy on tcp/8883 to rabbitmq:1883.
- gitea: HTTPS-only (DISABLE_SSH=true). No SSH port published.
MQTT split
- Remove stacks/mqtt placeholder.
- Add stacks/rabbitmq — general-purpose broker (AMQP + MQTT plugin),
used at both cloud and edge. External MQTT clients reach cloud broker
via nginx stream-proxy on 8883.
- Add stacks/mosquitto — reserved for the FROST (SensorThings) stack
only. Cloud-only. Internal to its own stack; no external ingress.
ML / notebooks (cloud-only)
- stacks/mlflow — experiment tracking + model registry. Postgres backend
on sql stack; local volume for artifacts (S3/MinIO is a TODO).
- stacks/jupyterhub — multi-user notebook server. DockerSpawner via
mounted docker.sock; users spawn into cloud-app network so they can
reach mlflow, influxdb (via grafana), rabbitmq.
Sites
- sites/gemaal1 — first edge deployment scaffold. Site-local override
template for binding nginx to PLANT_LAN_IP.
Docs
- README + docs/architecture.md updated: stacks table now lists 15 stacks,
ingress + attachment tables reflect mlflow/jupyterhub, TLS strategy
section locked, MQTT-split section added, Gitea HTTPS-only noted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:22:46 +02:00
2026-05-21 18:34:37 +00:00
## Auth
feat: SQL=postgres, nginx+certbot, MQTT split, ML stacks, gitea HTTPS-only, gemaal1 site
Round-2 changes locking in scaffold-phase decisions and adding ML/notebook stacks.
Locked decisions
- sql: postgres 16-alpine (was TBD); init.d/ mount for per-app DB provisioning
- nginx-proxy: stock nginx + certbot sidecar (was nginx:alpine TODO).
Chose stock over nginxproxy/nginx-proxy because stream{} is required for
MQTT-TLS reverse-proxy on tcp/8883 to rabbitmq:1883.
- gitea: HTTPS-only (DISABLE_SSH=true). No SSH port published.
MQTT split
- Remove stacks/mqtt placeholder.
- Add stacks/rabbitmq — general-purpose broker (AMQP + MQTT plugin),
used at both cloud and edge. External MQTT clients reach cloud broker
via nginx stream-proxy on 8883.
- Add stacks/mosquitto — reserved for the FROST (SensorThings) stack
only. Cloud-only. Internal to its own stack; no external ingress.
ML / notebooks (cloud-only)
- stacks/mlflow — experiment tracking + model registry. Postgres backend
on sql stack; local volume for artifacts (S3/MinIO is a TODO).
- stacks/jupyterhub — multi-user notebook server. DockerSpawner via
mounted docker.sock; users spawn into cloud-app network so they can
reach mlflow, influxdb (via grafana), rabbitmq.
Sites
- sites/gemaal1 — first edge deployment scaffold. Site-local override
template for binding nginx to PLANT_LAN_IP.
Docs
- README + docs/architecture.md updated: stacks table now lists 15 stacks,
ingress + attachment tables reflect mlflow/jupyterhub, TLS strategy
section locked, MQTT-split section added, Gitea HTTPS-only noted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:22:46 +02:00
2026-05-21 18:34:37 +00:00
Keycloak OIDC via `oauthenticator.generic.GenericOAuthenticator` . All authenticated users in the `wbd` realm can sign in (`c.GenericOAuthenticator.allow_all = True` ).
Admin promotion is currently driven by the `JUPYTERHUB_ADMIN_USERS` env (comma-separated emails). Switching to a Keycloak realm-role check (`app-admin` ) is a TODO.
## Spawner
**Current**: `SimpleLocalProcessSpawner` ("simple") — every user's notebook runs as a process inside the hub container itself, sharing the same filesystem. The spawner passes `--allow-root` to `jupyterhub-singleuser` because the hub container runs as root and the singleuser server refuses root without that flag.
This is fine for one or two operators but is **not ** the production-shape we want.
### TODO — switch to DockerSpawner
The repo wiring is already half-there:
- The `mgmt` network is mounted
- `/var/run/docker.sock` is mounted into the hub
- `DOCKER_NOTEBOOK_IMAGE` is set in `.env`
To switch, change `jupyterhub_config.py` :
```python
c.JupyterHub.spawner_class = "dockerspawner.DockerSpawner"
c.DockerSpawner.image = os.environ["DOCKER_NOTEBOOK_IMAGE"]
c.DockerSpawner.network_name = os.environ["DOCKER_NETWORK_NAME"]
c.DockerSpawner.notebook_dir = "/home/jovyan/work"
c.DockerSpawner.volumes = {"jupyter-user-{username}": "/home/jovyan/work"}
c.DockerSpawner.remove = True
```
…and add `dockerspawner` to the `Dockerfile` pip install.
## Other TODO
- Switch admin lookup from env-list to `app-admin` realm role
- Per-user persistent volume policy + size limits
feat: SQL=postgres, nginx+certbot, MQTT split, ML stacks, gitea HTTPS-only, gemaal1 site
Round-2 changes locking in scaffold-phase decisions and adding ML/notebook stacks.
Locked decisions
- sql: postgres 16-alpine (was TBD); init.d/ mount for per-app DB provisioning
- nginx-proxy: stock nginx + certbot sidecar (was nginx:alpine TODO).
Chose stock over nginxproxy/nginx-proxy because stream{} is required for
MQTT-TLS reverse-proxy on tcp/8883 to rabbitmq:1883.
- gitea: HTTPS-only (DISABLE_SSH=true). No SSH port published.
MQTT split
- Remove stacks/mqtt placeholder.
- Add stacks/rabbitmq — general-purpose broker (AMQP + MQTT plugin),
used at both cloud and edge. External MQTT clients reach cloud broker
via nginx stream-proxy on 8883.
- Add stacks/mosquitto — reserved for the FROST (SensorThings) stack
only. Cloud-only. Internal to its own stack; no external ingress.
ML / notebooks (cloud-only)
- stacks/mlflow — experiment tracking + model registry. Postgres backend
on sql stack; local volume for artifacts (S3/MinIO is a TODO).
- stacks/jupyterhub — multi-user notebook server. DockerSpawner via
mounted docker.sock; users spawn into cloud-app network so they can
reach mlflow, influxdb (via grafana), rabbitmq.
Sites
- sites/gemaal1 — first edge deployment scaffold. Site-local override
template for binding nginx to PLANT_LAN_IP.
Docs
- README + docs/architecture.md updated: stacks table now lists 15 stacks,
ingress + attachment tables reflect mlflow/jupyterhub, TLS strategy
section locked, MQTT-split section added, Gitea HTTPS-only noted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:22:46 +02:00
- CPU / memory limits per user container
2026-05-21 18:34:37 +00:00
- Idle-server culling (`jupyterhub-idle-culler` service)
- Project-specific notebook image with mlflow/influx/rabbitmq clients preinstalled