Skip to content

Commit 5bbe8c4

Browse files
committed
Support http-configurable and nginx reverse proxy
1 parent 8178110 commit 5bbe8c4

File tree

8 files changed

+259
-0
lines changed

8 files changed

+259
-0
lines changed

.github/workflows/test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
matrix:
3333
example:
3434
- basic-example
35+
- proxy-example
3536

3637
steps:
3738
- uses: actions/checkout@v4

proxy-example/.dockerignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tests/
2+
docker-compose.yaml

proxy-example/Dockerfile.jupyterhub

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
ARG JUPYTERHUB_VERSION
4+
FROM jupyterhub/jupyterhub:$JUPYTERHUB_VERSION
5+
6+
# Install dockerspawner, nativeauthenticator
7+
# hadolint ignore=DL3013
8+
RUN python3 -m pip install --no-cache-dir \
9+
dockerspawner \
10+
jupyterhub-nativeauthenticator
11+
12+
CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"]

proxy-example/README.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# JupyterLab Deployment with Configurable HTTP Proxy and Nginx Reverse Proxy
2+
3+
## Introduction
4+
5+
This project extends the existing JupyterLab deployment by introducing a robust proxy setup using both Configurable HTTP Proxy and Nginx Reverse Proxy. This configuration enhances the flexibility and security of the deployment.
6+
7+
## Improvements
8+
9+
- Added **Configurable HTTP Proxy** for managing the internal routing of JupyterLab services.
10+
- Integrated an **Nginx Reverse Proxy** layer for secure external access.
11+
- Provided a sample Nginx configuration optimized for this setup.
12+
13+
## Deployment
14+
15+
The deployment is managed with `docker-compose`, ensuring ease of setup and consistency. The `docker-compose.yml` includes the following services:
16+
17+
- `hub`: The main JupyterHub service.
18+
- `proxy`: The Configurable HTTP Proxy service.
19+
- `reverse-proxy`: The Nginx Reverse Proxy service.
20+
21+
## Usage
22+
23+
To deploy the JupyterLab environment:
24+
25+
1. Clone this repository.
26+
2. Navigate to the repository directory.
27+
3. Run `docker-compose up -d`.
28+
4. Once the deployment is complete, navigate to `http://localhost:8001` to access the JupyterHub interface through the Nginx Reverse Proxy.
29+
30+
## Contributing
31+
32+
Contributions are welcome! For major changes, please open an issue first to discuss what you would like to change.
33+
34+
## Acknowledgments
35+
36+
Improvement contributed by Hung Dinh Xuan @hungdinhxuan

proxy-example/docker-compose.yml

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
version: "3"
2+
3+
services:
4+
hub:
5+
build:
6+
context: .
7+
dockerfile: Dockerfile.jupyterhub
8+
args:
9+
JUPYTERHUB_VERSION: latest
10+
restart: always
11+
image: jupyterhub
12+
container_name: jupyterhub
13+
networks:
14+
- jupyterhub-network
15+
volumes:
16+
# The JupyterHub configuration file
17+
- "./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro"
18+
# Bind Docker socket on the host so we can connect to the daemon from
19+
# within the container
20+
- "/var/run/docker.sock:/var/run/docker.sock:rw"
21+
# Bind Docker volume on host for JupyterHub database and cookie secrets
22+
- "jupyterhub-data:/data"
23+
ports:
24+
- "8000:8000"
25+
environment:
26+
# This username will be a JupyterHub admin
27+
JUPYTERHUB_ADMIN: admin
28+
# All containers will join this network
29+
DOCKER_NETWORK_NAME: jupyterhub-network
30+
# JupyterHub will spawn this Notebook image for users
31+
DOCKER_NOTEBOOK_IMAGE: jupyter/base-notebook:latest
32+
# Notebook directory inside user image
33+
DOCKER_NOTEBOOK_DIR: /home/jovyan/work
34+
35+
proxy:
36+
image: jupyterhub/configurable-http-proxy:4.6.0
37+
container_name: jupyterhub-proxy
38+
restart: always
39+
networks:
40+
- jupyterhub-network
41+
ports:
42+
- "8002:8000"
43+
command: ["--default-target=http://hub:8000"]
44+
45+
reverse-proxy:
46+
image: nginx:1.25.3
47+
container_name: jupyterhub-reverse-proxy
48+
restart: always
49+
networks:
50+
- jupyterhub-network
51+
ports:
52+
- "8001:80"
53+
volumes:
54+
- ./nginx.conf:/etc/nginx/nginx.conf:ro
55+
depends_on:
56+
- proxy
57+
58+
volumes:
59+
jupyterhub-data:
60+
61+
networks:
62+
jupyterhub-network:
63+
name: jupyterhub-network

proxy-example/jupyterhub_config.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
# Configuration file for JupyterHub
5+
import os
6+
7+
c = get_config() # noqa: F821
8+
9+
# We rely on environment variables to configure JupyterHub so that we
10+
# avoid having to rebuild the JupyterHub container every time we change a
11+
# configuration parameter.
12+
13+
# Spawn single-user servers as Docker containers
14+
c.JupyterHub.spawner_class = "dockerspawner.DockerSpawner"
15+
16+
# Spawn containers from this image
17+
c.DockerSpawner.image = os.environ["DOCKER_NOTEBOOK_IMAGE"]
18+
19+
# Connect containers to this Docker network
20+
network_name = os.environ["DOCKER_NETWORK_NAME"]
21+
c.DockerSpawner.use_internal_ip = True
22+
c.DockerSpawner.network_name = network_name
23+
24+
# Explicitly set notebook directory because we'll be mounting a volume to it.
25+
# Most `jupyter/docker-stacks` *-notebook images run the Notebook server as
26+
# user `jovyan`, and set the notebook directory to `/home/jovyan/work`.
27+
# We follow the same convention.
28+
notebook_dir = os.environ.get("DOCKER_NOTEBOOK_DIR", "/home/jovyan/work")
29+
c.DockerSpawner.notebook_dir = notebook_dir
30+
31+
# Mount the real user's Docker volume on the host to the notebook user's
32+
# notebook directory in the container
33+
c.DockerSpawner.volumes = {"jupyterhub-user-{username}": notebook_dir}
34+
35+
# Remove containers once they are stopped
36+
c.DockerSpawner.remove = True
37+
38+
# For debugging arguments passed to spawned containers
39+
c.DockerSpawner.debug = True
40+
41+
# User containers will access hub by container name on the Docker network
42+
c.JupyterHub.hub_ip = "jupyterhub"
43+
c.JupyterHub.hub_port = 8080
44+
45+
# Persist hub data on volume mounted inside container
46+
c.JupyterHub.cookie_secret_file = "/data/jupyterhub_cookie_secret"
47+
c.JupyterHub.db_url = "sqlite:////data/jupyterhub.sqlite"
48+
49+
# Authenticate users with Native Authenticator
50+
c.JupyterHub.authenticator_class = "nativeauthenticator.NativeAuthenticator"
51+
52+
# Allow anyone to sign-up without approval
53+
c.NativeAuthenticator.open_signup = True
54+
55+
# Allowed admins
56+
admin = os.environ.get("JUPYTERHUB_ADMIN")
57+
if admin:
58+
c.Authenticator.admin_users = [admin]

proxy-example/nginx.conf

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
events {
2+
worker_connections 1024;
3+
}
4+
5+
http {
6+
server {
7+
listen 80;
8+
9+
location / {
10+
proxy_pass http://proxy:8000; # Assuming 'proxy' is the service name in docker-compose
11+
12+
# WebSocket support
13+
proxy_http_version 1.1;
14+
proxy_set_header Upgrade $http_upgrade;
15+
proxy_set_header Connection "upgrade";
16+
proxy_set_header Host $host;
17+
18+
# Handling real IP and scheme in proxied server
19+
proxy_set_header X-Real-IP $remote_addr;
20+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
21+
proxy_set_header X-Forwarded-Proto $scheme;
22+
proxy_set_header X-Forwarded-Host $host;
23+
proxy_set_header X-Forwarded-Port $server_port;
24+
}
25+
}
26+
}

proxy-example/tests/test_spawn.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from os import getenv
2+
import requests
3+
import pytest
4+
import time
5+
from uuid import uuid4
6+
7+
8+
TIMEOUT = 120
9+
10+
11+
@pytest.fixture(scope="session")
12+
def api_request():
13+
def _api_request(method, path, **kwargs):
14+
hub_url = getenv("HUB_URL", "http://localhost:8001").rstrip("/")
15+
m = getattr(requests, method)
16+
url = f"{hub_url}{path}"
17+
r = m(url, headers={"Authorization": "token test-token-123"}, **kwargs)
18+
r.raise_for_status()
19+
return r
20+
21+
return _api_request
22+
23+
24+
def wait_for_hub(api_request):
25+
# Wait for the hub to be ready
26+
now = time.time()
27+
try:
28+
api_request("get", "/hub/api")
29+
except requests.exceptions.RequestException:
30+
if time.time() - now > TIMEOUT:
31+
raise TimeoutError(f"Hub did not start in {TIMEOUT} seconds")
32+
time.sleep(5)
33+
34+
35+
def test_create_user_and_server(api_request):
36+
wait_for_hub(api_request)
37+
38+
# Create a new user
39+
username = str(uuid4())
40+
api_request("post", "/hub/api/users", json={"usernames": [username]})
41+
42+
# Start a server for the user
43+
api_request("post", f"/hub/api/users/{username}/server")
44+
45+
# Wait for the server
46+
ready = False
47+
now = time.time()
48+
while not ready:
49+
wait_r = api_request("get", f"/hub/api/users/{username}").json()
50+
if wait_r["servers"][""]["ready"]:
51+
ready = True
52+
break
53+
if time.time() - now > TIMEOUT:
54+
raise TimeoutError(f"Singleuser server did not start in {TIMEOUT} seconds")
55+
time.sleep(5)
56+
57+
# Call the jupyter-server API
58+
server_r = api_request("get", f"/user/{username}/api").json()
59+
assert "version" in server_r
60+
contents_r = api_request("get", f"/user/{username}/api/contents").json()
61+
assert "content" in contents_r

0 commit comments

Comments
 (0)