There have been many, many posts about this and I just don’t understand if it’s a configuration issue or whatever, but as much as I try to get my nonvc extraContainer which is exposed as localhost:8080 natively on my single-user, I can access this novnc simply by using hub/user/user/user-name/proxy/8080. Please, I need urgent help to be able to make this work.
As much as I have been researching and researching, I have not been able to come up with a good source of knowledge or a didactic example to help set this up right. Thank you very much.
Hub custom image:
FROM quay.io/jupyterhub/k8s-hub:3.1.0
USER root
# Instala el cliente de Kubernetes
RUN pip install kubernetes
# Instalamos
RUN pip install jupyter-server-proxy
# Copia tu FastAPI (si lo necesitas)
COPY ./service-fastapi /usr/src/fastapi
RUN python3 -m pip install -r /usr/src/fastapi/requirements.txt
USER ${NB_USER}
Single-user image:
FROM ros:jazzy
ENV DEBIAN_FRONTEND=noninteractive
ENV PATH="/opt/venv/bin:$PATH"
ENV DISPLAY=novnc:0.0
# Instalar dependencias del sistema y entorno Python
RUN apt-get update && \
apt-get install -y --no-install-recommends \
python3-pip \
python3-venv \
nodejs \
npm \
curl \
wget \
httpie \
sudo \
bash \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
python3 -m venv /opt/venv && \
/opt/venv/bin/pip install --upgrade pip && \
/opt/venv/bin/pip install \
jupyterhub==4.* \
notebook==7.* \
jupyter-server-proxy
# Crear usuario jovyan con permisos sudo sin contraseña
RUN useradd -m -s /bin/bash jovyan && \
echo "jovyan ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Usar bash como shell por defecto para jovyan
SHELL ["/bin/bash", "-c"]
# Configuración de entorno para jovyan
USER jovyan
WORKDIR /home/jovyan
# Activar entorno ROS en bashrc
RUN echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc
CMD ["jupyterhub-singleuser"]
config.yaml:
proxy:
service:
type: LoadBalancer
singleuser:
image:
name: gcr.io/isis25-03-load-balancer/singleuser-custom
tag: latest
cmd: null
cpu:
limit: 1
guarantee: 0.5
memory:
limit: 1G
guarantee: 512M
storage:
capacity: 1Gi
nodeSelector:
hub.jupyter.org/node-purpose: user
extraTolerations:
- key: "hub.jupyter.org/dedicated"
operator: "Equal"
value: "user"
effect: "NoSchedule"
extraContainers:
- name: novnc
image: theasp/novnc:latest
ports:
- containerPort: 8080
env:
- name: DISPLAY_WIDTH
value: "500"
- name: DISPLAY_HEIGHT
value: "500"
- name: RUN_XTERM
value: "no"
networkPolicy:
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: jupyterhub
component: singleuser-server
ports:
- port: 8080
scheduling:
userScheduler:
enabled: true
hub:
image:
name: gcr.io/isis25-03-load-balancer/jupyterhub-custom
tag: latest
services:
fastapi:
url: http://hub:8181
command:
- /usr/local/bin/uvicorn
- app:app
- --port
- "8181"
- --host
- "0.0.0.0"
- --app-dir
- /usr/src/fastapi
oauth_redirect_uri: http://35.232.148.6/services/fastapi/oauth_callback
environment:
PUBLIC_HOST: http://35.232.148.6
networkPolicy:
ingress:
- ports:
- port: 8181
from:
- podSelector:
matchLabels:
hub.jupyter.org/network-access-hub: "true"
service:
extraPorts:
- port: 8181
targetPort: 8181
name: fastapi
config:
JupyterHub:
allow_named_servers: true
services:
- name: fastapi
admin: true
load_roles:
- name: "service-with-admin-users"
services: ["fastapi"]
scopes: ["servers", "admin:users"]
- name: "user"
scopes: ["servers", "self", "access:services"]
- name: "admin-role-for-testuser"
users: ["test-user"]
scopes: ["servers", "admin:users"]
KubeSpawner:
modify_pod_hook: null # Esto se sobrescribirá con extraConfig
extraConfig:
00-add-novnc-values: |
import os
# Obtiene variables de entorno definidas por JupyterHub
hub_host = os.environ.get("JUPYTERHUB_HOST", "localhost")
hub_user = os.environ.get("JUPYTERHUB_USER", "unknown")
# Construye la URL para acceder a noVNC a través del proxy
novnc_url = "http://{}/user/{}/proxy/novnc/".format(hub_host, hub_user)
print("NOVNC_URL configured as:", novnc_url)
# Establece la variable en el entorno del singleuser
c.KubeSpawner.environment.setdefault("NOVNC_URL", novnc_url)
20-novnc-proxy-config: |
# Registra el servicio noVNC en el proxy de JupyterHub
c.ServerProxy.servers = {
'novnc': {
'command': [], # Se asume que el contenedor extra ya está corriendo noVNC
'port': 8080,
'absolute_url': True,
'timeout': 30,
'launcher_entry': {
'title': 'noVNC',
'icon_path': '/usr/share/icons/hicolor/48x48/apps/novnc.png' # Opcional
}
}
}
proxy:
chp:
networkPolicy:
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: jupyterhub
app.kubernetes.io/component: hub
ports:
- port: 8181
Some logs:
No config at /usr/local/etc/jupyterhub/existing-secret/values.yaml
Loading extra config: 00-add-novnc-values
NOVNC_URL configured as: http://localhost/user/unknown/proxy/novnc/
Loading extra config: 20-novnc-proxy-config
Loading extra config: 25-novnc-debug
Registered proxy servers: <LazyConfigValue {'update': {'novnc': {'command': [], 'port': 8080, 'absolute_url': True, 'timeout': 30, 'launcher_entry': {'title': 'noVNC', 'icon_path': '/usr/share/icons/hicolor/48x48/apps/novnc.png'}}}}>
[I 2025-04-05 02:36:14.037 JupyterHub app:2859] Running JupyterHub version 4.0.2
[I 2025-04-05 02:36:14.037 JupyterHub app:2889] Using Authenticator: jupyterhub.auth.DummyAuthenticator-4.0.2
[I 2025-04-05 02:36:14.037 JupyterHub app:2889] Using Spawner: kubespawner.spawner.KubeSpawner-6.1.0
[I 2025-04-05 02:36:14.037 JupyterHub app:2889] Using Proxy: jupyterhub.proxy.ConfigurableHTTPProxy-4.0.2
[I 2025-04-05 02:36:14.136 JupyterHub app:1984] Not using allowed_users. Any authenticated user will be allowed.
[I 2025-04-05 02:36:14.175 JupyterHub provider:661] Updating oauth client service-fastapi
[I 2025-04-05 02:36:14.260 JupyterHub reflector:282] watching for pods with label selector='component=singleuser-server' in namespace jhub
[W 2025-04-05 02:36:14.267 JupyterHub _version:37] Single-user server has no version header, which means it is likely < 0.8. Expected 4.0.2
[I 2025-04-05 02:36:14.268 JupyterHub app:2573] hola still running
[I 2025-04-05 02:36:14.268 JupyterHub app:2928] Initialized 1 spawners in 0.036 seconds
[I 2025-04-05 02:36:14.275 JupyterHub metrics:278] Found 1 active users in the last ActiveUserPeriods.twenty_four_hours
[I 2025-04-05 02:36:14.276 JupyterHub metrics:278] Found 1 active users in the last ActiveUserPeriods.seven_days
[I 2025-04-05 02:36:14.277 JupyterHub metrics:278] Found 1 active users in the last ActiveUserPeriods.thirty_days
[I 2025-04-05 02:36:14.277 JupyterHub app:3142] Not starting proxy
[I 2025-04-05 02:36:14.282 JupyterHub app:3178] Hub API listening on http://:8081/hub/
[I 2025-04-05 02:36:14.283 JupyterHub app:3180] Private Hub API connect url http://hub:8081/hub/
[I 2025-04-05 02:36:14.283 JupyterHub app:3189] Starting managed service jupyterhub-idle-culler
[I 2025-04-05 02:36:14.283 JupyterHub service:385] Starting service 'jupyterhub-idle-culler': ['python3', '-m', 'jupyterhub_idle_culler', '--url=http://localhost:8081/hub/api', '--timeout=3600', '--cull-every=600', '--concurrency=10']
[I 2025-04-05 02:36:14.284 JupyterHub service:133] Spawning python3 -m jupyterhub_idle_culler --url=http://localhost:8081/hub/api --timeout=3600 --cull-every=600 --concurrency=10
[I 2025-04-05 02:36:14.285 JupyterHub app:3189] Starting managed service fastapi at http://hub:8181
[I 2025-04-05 02:36:14.285 JupyterHub service:385] Starting service 'fastapi': ['/usr/local/bin/uvicorn', 'app:app', '--port', '8181', '--host', '0.0.0.0', '--app-dir', '/usr/src/fastapi']
[I 2025-04-05 02:36:14.288 JupyterHub service:133] Spawning /usr/local/bin/uvicorn app:app --port 8181 --host 0.0.0.0 --app-dir /usr/src/fastapi
[I 2025-04-05 02:36:15.515 JupyterHub log:191] 200 GET /hub/api/ (jupyterhub-idle-culler@127.0.0.1) 10.22ms
[W 2025-04-05 02:36:15.522 JupyterHub scopes:881] Not authorizing access to /hub/api/users. Requires any of [list:users], not derived from scopes []
[W 2025-04-05 02:36:15.522 JupyterHub web:1869] 403 GET /hub/api/users?state=ready (127.0.0.1): Action is not authorized with current scopes; requires any of [list:users]
[W 2025-04-05 02:36:15.523 JupyterHub log:191] 403 GET /hub/api/users?state=[secret] (jupyterhub-idle-culler@127.0.0.1) 4.12ms
[E 250405 02:36:15 ioloop:758] Exception in callback functools.partial(<bound method IOLoop._discard_future_result of <tornado.platform.asyncio.AsyncIOMainLoop object at 0x7a03f9564910>>, <Task finished name='Task-1' coro=<cull_idle() done, defined at /usr/local/lib/python3.11/site-packages/jupyterhub_idle_culler/__init__.py:73> exception=HTTP 403: Forbidden>)
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/tornado/ioloop.py", line 738, in _run_callback
ret = callback()
^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/tornado/ioloop.py", line 762, in _discard_future_result
future.result()
File "/usr/local/lib/python3.11/site-packages/jupyterhub_idle_culler/__init__.py", line 422, in cull_idle
async for user in fetch_paginated(req):
File "/usr/local/lib/python3.11/site-packages/jupyterhub_idle_culler/__init__.py", line 135, in fetch_paginated
response = await resp_future
^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/jupyterhub_idle_culler/__init__.py", line 117, in fetch
return await client.fetch(req)
^^^^^^^^^^^^^^^^^^^^^^^
tornado.httpclient.HTTPClientError: HTTP 403: Forbidden
INFO: Started server process [13]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8181 (Press CTRL+C to quit)
INFO: 10.28.1.1:49175 - "GET /services/fastapi/ HTTP/1.1" 200 OK
[W 2025-04-05 02:36:15.654 JupyterHub proxy:445] Adding missing route for fastapi (Server(url=http://hub:8181/services/fastapi/, bind_url=http://hub:8181/services/fastapi/))
[I 2025-04-05 02:36:15.656 JupyterHub proxy:311] Adding service fastapi to proxy /services/fastapi/ => http://hub:8181
[I 2025-04-05 02:36:15.662 JupyterHub app:3247] JupyterHub is now running, internal Hub API at http://hub:8081/hub/