From 5bbe8c423052b8bce038402ce135e0d34d0a4bef Mon Sep 17 00:00:00 2001 From: Hung Dinh Xuan Date: Wed, 22 Nov 2023 22:04:21 +0900 Subject: [PATCH 1/2] Support http-configurable and nginx reverse proxy --- .github/workflows/test.yml | 1 + proxy-example/.dockerignore | 2 + proxy-example/Dockerfile.jupyterhub | 12 ++++++ proxy-example/README.md | 36 +++++++++++++++++ proxy-example/docker-compose.yml | 63 +++++++++++++++++++++++++++++ proxy-example/jupyterhub_config.py | 58 ++++++++++++++++++++++++++ proxy-example/nginx.conf | 26 ++++++++++++ proxy-example/tests/test_spawn.py | 61 ++++++++++++++++++++++++++++ 8 files changed, 259 insertions(+) create mode 100644 proxy-example/.dockerignore create mode 100644 proxy-example/Dockerfile.jupyterhub create mode 100644 proxy-example/README.md create mode 100644 proxy-example/docker-compose.yml create mode 100644 proxy-example/jupyterhub_config.py create mode 100644 proxy-example/nginx.conf create mode 100644 proxy-example/tests/test_spawn.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b65c2b66..501db66d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,7 @@ jobs: matrix: example: - basic-example + - proxy-example steps: - uses: actions/checkout@v4 diff --git a/proxy-example/.dockerignore b/proxy-example/.dockerignore new file mode 100644 index 00000000..1b8087bc --- /dev/null +++ b/proxy-example/.dockerignore @@ -0,0 +1,2 @@ +tests/ +docker-compose.yaml diff --git a/proxy-example/Dockerfile.jupyterhub b/proxy-example/Dockerfile.jupyterhub new file mode 100644 index 00000000..b7b437ed --- /dev/null +++ b/proxy-example/Dockerfile.jupyterhub @@ -0,0 +1,12 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +ARG JUPYTERHUB_VERSION +FROM jupyterhub/jupyterhub:$JUPYTERHUB_VERSION + +# Install dockerspawner, nativeauthenticator +# hadolint ignore=DL3013 +RUN python3 -m pip install --no-cache-dir \ + dockerspawner \ + jupyterhub-nativeauthenticator + +CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"] diff --git a/proxy-example/README.md b/proxy-example/README.md new file mode 100644 index 00000000..6389305d --- /dev/null +++ b/proxy-example/README.md @@ -0,0 +1,36 @@ +# JupyterLab Deployment with Configurable HTTP Proxy and Nginx Reverse Proxy + +## Introduction + +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. + +## Improvements + +- Added **Configurable HTTP Proxy** for managing the internal routing of JupyterLab services. +- Integrated an **Nginx Reverse Proxy** layer for secure external access. +- Provided a sample Nginx configuration optimized for this setup. + +## Deployment + +The deployment is managed with `docker-compose`, ensuring ease of setup and consistency. The `docker-compose.yml` includes the following services: + +- `hub`: The main JupyterHub service. +- `proxy`: The Configurable HTTP Proxy service. +- `reverse-proxy`: The Nginx Reverse Proxy service. + +## Usage + +To deploy the JupyterLab environment: + +1. Clone this repository. +2. Navigate to the repository directory. +3. Run `docker-compose up -d`. +4. Once the deployment is complete, navigate to `http://localhost:8001` to access the JupyterHub interface through the Nginx Reverse Proxy. + +## Contributing + +Contributions are welcome! For major changes, please open an issue first to discuss what you would like to change. + +## Acknowledgments + +Improvement contributed by Hung Dinh Xuan @hungdinhxuan diff --git a/proxy-example/docker-compose.yml b/proxy-example/docker-compose.yml new file mode 100644 index 00000000..8b64397c --- /dev/null +++ b/proxy-example/docker-compose.yml @@ -0,0 +1,63 @@ +version: "3" + +services: + hub: + build: + context: . + dockerfile: Dockerfile.jupyterhub + args: + JUPYTERHUB_VERSION: latest + restart: always + image: jupyterhub + container_name: jupyterhub + networks: + - jupyterhub-network + volumes: + # The JupyterHub configuration file + - "./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro" + # Bind Docker socket on the host so we can connect to the daemon from + # within the container + - "/var/run/docker.sock:/var/run/docker.sock:rw" + # Bind Docker volume on host for JupyterHub database and cookie secrets + - "jupyterhub-data:/data" + ports: + - "8000:8000" + environment: + # This username will be a JupyterHub admin + JUPYTERHUB_ADMIN: admin + # All containers will join this network + DOCKER_NETWORK_NAME: jupyterhub-network + # JupyterHub will spawn this Notebook image for users + DOCKER_NOTEBOOK_IMAGE: jupyter/base-notebook:latest + # Notebook directory inside user image + DOCKER_NOTEBOOK_DIR: /home/jovyan/work + + proxy: + image: jupyterhub/configurable-http-proxy:4.6.0 + container_name: jupyterhub-proxy + restart: always + networks: + - jupyterhub-network + ports: + - "8002:8000" + command: ["--default-target=http://hub:8000"] + + reverse-proxy: + image: nginx:1.25.3 + container_name: jupyterhub-reverse-proxy + restart: always + networks: + - jupyterhub-network + ports: + - "8001:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - proxy + +volumes: + jupyterhub-data: + +networks: + jupyterhub-network: + name: jupyterhub-network diff --git a/proxy-example/jupyterhub_config.py b/proxy-example/jupyterhub_config.py new file mode 100644 index 00000000..028f65cb --- /dev/null +++ b/proxy-example/jupyterhub_config.py @@ -0,0 +1,58 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +# Configuration file for JupyterHub +import os + +c = get_config() # noqa: F821 + +# We rely on environment variables to configure JupyterHub so that we +# avoid having to rebuild the JupyterHub container every time we change a +# configuration parameter. + +# Spawn single-user servers as Docker containers +c.JupyterHub.spawner_class = "dockerspawner.DockerSpawner" + +# Spawn containers from this image +c.DockerSpawner.image = os.environ["DOCKER_NOTEBOOK_IMAGE"] + +# Connect containers to this Docker network +network_name = os.environ["DOCKER_NETWORK_NAME"] +c.DockerSpawner.use_internal_ip = True +c.DockerSpawner.network_name = network_name + +# Explicitly set notebook directory because we'll be mounting a volume to it. +# Most `jupyter/docker-stacks` *-notebook images run the Notebook server as +# user `jovyan`, and set the notebook directory to `/home/jovyan/work`. +# We follow the same convention. +notebook_dir = os.environ.get("DOCKER_NOTEBOOK_DIR", "/home/jovyan/work") +c.DockerSpawner.notebook_dir = notebook_dir + +# Mount the real user's Docker volume on the host to the notebook user's +# notebook directory in the container +c.DockerSpawner.volumes = {"jupyterhub-user-{username}": notebook_dir} + +# Remove containers once they are stopped +c.DockerSpawner.remove = True + +# For debugging arguments passed to spawned containers +c.DockerSpawner.debug = True + +# User containers will access hub by container name on the Docker network +c.JupyterHub.hub_ip = "jupyterhub" +c.JupyterHub.hub_port = 8080 + +# Persist hub data on volume mounted inside container +c.JupyterHub.cookie_secret_file = "/data/jupyterhub_cookie_secret" +c.JupyterHub.db_url = "sqlite:////data/jupyterhub.sqlite" + +# Authenticate users with Native Authenticator +c.JupyterHub.authenticator_class = "nativeauthenticator.NativeAuthenticator" + +# Allow anyone to sign-up without approval +c.NativeAuthenticator.open_signup = True + +# Allowed admins +admin = os.environ.get("JUPYTERHUB_ADMIN") +if admin: + c.Authenticator.admin_users = [admin] diff --git a/proxy-example/nginx.conf b/proxy-example/nginx.conf new file mode 100644 index 00000000..5532fd77 --- /dev/null +++ b/proxy-example/nginx.conf @@ -0,0 +1,26 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 80; + + location / { + proxy_pass http://proxy:8000; # Assuming 'proxy' is the service name in docker-compose + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + + # Handling real IP and scheme in proxied server + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + } + } +} \ No newline at end of file diff --git a/proxy-example/tests/test_spawn.py b/proxy-example/tests/test_spawn.py new file mode 100644 index 00000000..27ff9895 --- /dev/null +++ b/proxy-example/tests/test_spawn.py @@ -0,0 +1,61 @@ +from os import getenv +import requests +import pytest +import time +from uuid import uuid4 + + +TIMEOUT = 120 + + +@pytest.fixture(scope="session") +def api_request(): + def _api_request(method, path, **kwargs): + hub_url = getenv("HUB_URL", "http://localhost:8001").rstrip("/") + m = getattr(requests, method) + url = f"{hub_url}{path}" + r = m(url, headers={"Authorization": "token test-token-123"}, **kwargs) + r.raise_for_status() + return r + + return _api_request + + +def wait_for_hub(api_request): + # Wait for the hub to be ready + now = time.time() + try: + api_request("get", "/hub/api") + except requests.exceptions.RequestException: + if time.time() - now > TIMEOUT: + raise TimeoutError(f"Hub did not start in {TIMEOUT} seconds") + time.sleep(5) + + +def test_create_user_and_server(api_request): + wait_for_hub(api_request) + + # Create a new user + username = str(uuid4()) + api_request("post", "/hub/api/users", json={"usernames": [username]}) + + # Start a server for the user + api_request("post", f"/hub/api/users/{username}/server") + + # Wait for the server + ready = False + now = time.time() + while not ready: + wait_r = api_request("get", f"/hub/api/users/{username}").json() + if wait_r["servers"][""]["ready"]: + ready = True + break + if time.time() - now > TIMEOUT: + raise TimeoutError(f"Singleuser server did not start in {TIMEOUT} seconds") + time.sleep(5) + + # Call the jupyter-server API + server_r = api_request("get", f"/user/{username}/api").json() + assert "version" in server_r + contents_r = api_request("get", f"/user/{username}/api/contents").json() + assert "content" in contents_r From 9853435fa9e127208ce3511b53384df900200702 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:23:28 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- proxy-example/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy-example/nginx.conf b/proxy-example/nginx.conf index 5532fd77..6a83fa1c 100644 --- a/proxy-example/nginx.conf +++ b/proxy-example/nginx.conf @@ -23,4 +23,4 @@ http { proxy_set_header X-Forwarded-Port $server_port; } } -} \ No newline at end of file +}