From d7e7733166a1cd46d29941648dc288906d3aea3d Mon Sep 17 00:00:00 2001 From: DerGrumpf Date: Tue, 15 Jul 2025 18:40:59 +0200 Subject: [PATCH] Added Nbgrader --- .gitignore | 22 ++ compose.override.yml | 39 +++ compose.yml | 10 + flake.lock | 61 ++++ flake.nix | 161 +++++++++++ hub/postgres/compose.yml | 17 -- {hub/jhub => jupyter-share}/compose.yml | 0 jupyterhub/Dockerfile | 48 ++++ jupyterhub/compose.yml | 15 + jupyterhub/conf_test.py | 9 + jupyterhub/jupyterhub_config.py | 227 +++++++++++++++ jupyterhub/nbgrader/nbgrader_config.py | 30 ++ jupyterhub/nbgrader_config.py | 30 ++ jupyterhub/templates/login.html | 74 +++++ jupyterhub/templates/logo.png | Bin 0 -> 11257 bytes jupyterhub/templates/page.html | 61 ++++ jupyterhub/templates/signup.html | 82 ++++++ nginx/compose.yml | 15 + nginx/conf.d/jupyterhub.conf | 43 +++ nginx/discord.html | 360 ++++++++++++++++++++++++ nginx/nginx.conf | 30 ++ nginx/snippets/proxy-headers.conf | 9 + postgres/compose.yml | 22 ++ postgres/init-multiple-db.sh | 0 postgres/postgresql.conf | 6 + 25 files changed, 1354 insertions(+), 17 deletions(-) create mode 100644 .gitignore create mode 100644 compose.override.yml create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 hub/postgres/compose.yml rename {hub/jhub => jupyter-share}/compose.yml (100%) create mode 100644 jupyterhub/Dockerfile create mode 100644 jupyterhub/compose.yml create mode 100644 jupyterhub/conf_test.py create mode 100644 jupyterhub/jupyterhub_config.py create mode 100644 jupyterhub/nbgrader/nbgrader_config.py create mode 100644 jupyterhub/nbgrader_config.py create mode 100644 jupyterhub/templates/login.html create mode 100644 jupyterhub/templates/logo.png create mode 100644 jupyterhub/templates/page.html create mode 100644 jupyterhub/templates/signup.html create mode 100644 nginx/compose.yml create mode 100644 nginx/conf.d/jupyterhub.conf create mode 100644 nginx/discord.html create mode 100644 nginx/nginx.conf create mode 100644 nginx/snippets/proxy-headers.conf create mode 100644 postgres/compose.yml create mode 100644 postgres/init-multiple-db.sh create mode 100644 postgres/postgresql.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a157c00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# --- This Project ---# +data/ +logs/ +.env + +# Docker secrets +/secrets/ +*.secret +*.key +*.pem +*.crt + +# --- OS & Temp Files --- +.DS_Store +Thumbs.db +*.swp +*.swo +*.tmp +*~ +~$* + + diff --git a/compose.override.yml b/compose.override.yml new file mode 100644 index 0000000..bec2ca3 --- /dev/null +++ b/compose.override.yml @@ -0,0 +1,39 @@ +services: + jupyterhub: + container_name: ${COMPOSE_PROJECT_NAME}-jupyterhub + environment: + - POSTGRES_HOST=postgres + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + + - JUPYTERHUB_CRYPT_KEY=${JUPYTERHUB_CRYPT_KEY} + - DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME} + - NBGRADER_DB_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${NBGRADER_DB} + ports: + - ${JUPYTERHUB_PORT:-8000}:8000 + volumes: + - ./data/jupyter:/srv/jupyterhub/data + networks: + - jupyterhub-network + depends_on: + - postgres + + postgres: + container_name: ${COMPOSE_PROJECT_NAME}-postgres + ports: + - ${POSTGRES_PORT:-5432}:5432 + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - jupyterhub-network + + nginx: + container_name: ${COMPOSE_PROJECT_NAME}-nginx + ports: + - ${NGINX_HTTP_PORT:-80}:80 + - ${NGINX_HTTPS_PORT:-443}:443 + networks: + - jupyterhub-network + depends_on: + - jupyterhub diff --git a/compose.yml b/compose.yml index e69de29..f46cef8 100644 --- a/compose.yml +++ b/compose.yml @@ -0,0 +1,10 @@ +include: + - jupyterhub/compose.yml + - postgres/compose.yml + - nginx/compose.yml + +networks: + jupyterhub-network: + name: ${DOCKER_NETWORK_NAME:-jupyterhub-network} + driver: bridge + attachable: true diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5ca09a9 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1750776420, + "narHash": "sha256-/CG+w0o0oJ5itVklOoLbdn2dGB0wbZVOoDm4np6w09A=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "30a61f056ac492e3b7cdcb69c1e6abdcf00e39cf", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d5c8c56 --- /dev/null +++ b/flake.nix @@ -0,0 +1,161 @@ +{ + description = "Docker JupyterHub development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + app = { + setup-dev = pkgs.writeShellScriptBin "setup-dev" '' + echo "Setting up basic Project Structure" + echo "----------------------------------" + echo "Setting up logs directory" + mkdir -p logs + mkdir -p logs/nginx + mkdir -p logs/jupyterhub + mkdir -p logs/postgres + echo "Logs Directory:" + tree logs/ + echo "" + echo "Setting up jupyter-data" + mkdir -p jupyter-data/nbgrader/courses/example-course/{autograded,feedback,release,source/assigment1,submitted} + mkdir -p jupyter-data/nbgrader/{exchange,users} + cat > jupyter-data/nbgrader/courses/example-course/source/assigment1/sample.ipynb << 'EOL' + { + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Sample Notebook\n", + "## Created with Bash\n", + "\n", + "This Jupyter notebook was generated automatically using a bash script." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Python code example\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "x = np.linspace(0, 10, 100)\n", + "y = np.sin(x)\n", + "\n", + "plt.plot(x, y)\n", + "plt.title('Sine Wave')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next Steps\n", + "\n", + "- Run the code cells\n", + "- Add more cells\n", + "- Experiment with different Python libraries" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 + } + EOL + echo "jupyter-data Directory:" + tree jupyter-data + ''; + + storage = pkgs.writeShellScriptBin "storage" '' + echo "Creating storage" + mkdir -p storage/{jupyter,postgres} + chmod -R 644 storage/ + ''; + + build = pkgs.writeShellScriptBin "build" '' + echo "Building all Containers" + ''; + + clean = pkgs.writeShellScriptBin "clean" '' + echo "Cleaning Project" + echo "----------------------------------" + echo "Removing logs" + rm -rf logs/ + echo "Removing jupyter-data" + rm -rf jupyter-data/ + ''; + + reset = pkgs.writeShellScriptBin "reset" '' + ${app.clean}/bin/clean + ${app.setup-dev}/bin/setup-dev + ''; + }; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + # For deployment/testing + docker + docker-compose + + # Terminal Tools + tree + ] ++ (builtins.attrValues app); + + shellHook = '' + echo "JupyterHub dev shell" + echo "------------------------------------" + echo "$(docker --version)" + echo "------------------------------------" + echo "Available commands:" + echo " setup-dev - Set Up dev environment" + echo " clean - Clean the Project" + echo " reset - Reset project completly (Use with caution it whipes all generated Data)" + ''; + }; + + apps = { + setup-dev = { + type = "app"; + program = "${app.setup-dev}/bin/setup-dev"; + }; + + build = { + type = "app"; + program = "${app.build}/bin/build"; + }; + + default = self.apps.${system}.dev; + }; + }); +} diff --git a/hub/postgres/compose.yml b/hub/postgres/compose.yml deleted file mode 100644 index b7fc090..0000000 --- a/hub/postgres/compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -services: - db: - image: postgres - restart: always - environment: - POSTRGES_PASSWORD: 123456 - volumes: - - pgdata:/var/lib/postgresql/data - - adminer: - image: adminer - restart: always - ports: - - 8080:8080 - -volumes: - pgdata: diff --git a/hub/jhub/compose.yml b/jupyter-share/compose.yml similarity index 100% rename from hub/jhub/compose.yml rename to jupyter-share/compose.yml diff --git a/jupyterhub/Dockerfile b/jupyterhub/Dockerfile new file mode 100644 index 0000000..c2bc488 --- /dev/null +++ b/jupyterhub/Dockerfile @@ -0,0 +1,48 @@ +FROM quay.io/jupyterhub/jupyterhub:latest + +# Install system dependencies +USER root +RUN apt-get update && apt-get install -y \ + curl \ + gnupg \ + iproute2 \ + iputils-ping \ + netcat \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install Docker CLI (needed for DockerSpawner) +RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ + && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu focal stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt-get update \ + && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# Install Required packages +RUN pip install --no-cache-dir \ + dockerspawner \ + jupyterhub-nativeauthenticator \ + jupyterhub-dummyauthenticator \ + nbgrader \ + psycopg2-binary + +RUN mkdir -p /srv/nbgrader/exchange +RUN chmod -R 777 /srv/nbgrader/exchange +RUN mkdir -p /srv/nbgrader/courses +RUN chown -R 1000:100 /srv/nbgrader + +#RUN jupyter nbextension install --sys-prefix --py nbgrader --overwrite +#RUN jupyter nbextension enable --sys-prefix --py nbgrader +#RUN jupyter serverextension enable --sys-prefix --py nbgrader + +# Set Workdir +WORKDIR /srv/jupyterhub + +# Generate Cookie Secret +RUN openssl rand -hex 32 > /srv/jupyterhub/cookie_secret +RUN chmod 600 /srv/jupyterhub/cookie_secret +#ENV JUPYTERHUB_AUTH_TOKEN=$(openssl rand -hex 32) + +EXPOSE 8000 + +CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"] diff --git a/jupyterhub/compose.yml b/jupyterhub/compose.yml new file mode 100644 index 0000000..89cc6f3 --- /dev/null +++ b/jupyterhub/compose.yml @@ -0,0 +1,15 @@ +services: + jupyterhub: + build: . + container_name: jupyterhub + restart: unless-stopped + ports: + - 8000:8000 + environment: + - JUPYTERHUB_LOG_LEVEL=DEBUG + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./templates:/srv/jupyterhub/templates:ro + - ./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py + - ./nbgrader_config.py:/srv/nbgrader/courses/nbgrader_config.py + - ../logs/jupyterhub:/var/log/jupyterhub diff --git a/jupyterhub/conf_test.py b/jupyterhub/conf_test.py new file mode 100644 index 0000000..345e513 --- /dev/null +++ b/jupyterhub/conf_test.py @@ -0,0 +1,9 @@ +c = get_config() + +c.JupyterHub.authenticator_class = "dummy" +c.JupyterHub.spawner_class = "docker" +c.JupyterHub.hub_ip = '0.0.0.0' +c.JupyterHub.hub_connect_ip = 'jupyterhub' +c.DockerSpawner.image = 'jupyter/base-notebook' +c.DockerSpawner.network_name = 'stack_default' +c.DockerSpawner.remove = True diff --git a/jupyterhub/jupyterhub_config.py b/jupyterhub/jupyterhub_config.py new file mode 100644 index 0000000..16b09f2 --- /dev/null +++ b/jupyterhub/jupyterhub_config.py @@ -0,0 +1,227 @@ +import os +import sys +from dockerspawner import DockerSpawner +from jupyterhub.utils import random_port +from nativeauthenticator import NativeAuthenticator +#from nbgrader.auth import JupyterHubAuthPlugin +from traitlets import Unicode, Bool, Int, Float + +import secrets +import string +from datetime import datetime, timedelta +from typing import Optional + +def generate_api_token( + length: int = 32, + prefix: Optional[str] = None, +) -> dict: + """ + Generate a secure random API token with optional prefix and expiration. + + Args: + length: Length of the token (default: 32) + prefix: Optional prefix for the token (e.g., 'hub_') + expiration_days: Optional expiration in days from now + + Returns: + - token: The generated token + """ + # Generate cryptographically secure random string + alphabet = string.ascii_letters + string.digits + token = ''.join(secrets.choice(alphabet) for _ in range(length)) + + # Add prefix if specified + if prefix: + token = f"{prefix}{token}" + + return token + +import subprocess + +def is_service_available_cmd(host, port): + """Check service using system commands""" + try: + subprocess.run( + ['nc', '-z', host, str(port)], + check=True, + timeout=3, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + return True + except (subprocess.TimeoutExpired, subprocess.CalledProcessError): + return False + +# Configuration file for JupyterHub +c = get_config() + +# Base URL configuration (The new way, dont use ip/port/bind_url attributes it throws a warning and is deprecated) +port = int(os.environ.get('JUPYTERHUB_PORT', '8000')) +base_url = os.environ.get('JUPYTERHUB_BASE_URL', '/jupyter/') +c.JupyterHub.bind_url = f"http://0.0.0.0:{port}{base_url}" + +# Database configuration +c.JupyterHub.db_url = f"postgresql://{os.environ['POSTGRES_USER']}:{os.environ['POSTGRES_PASSWORD']}@stack-{os.environ['POSTGRES_HOST']}:5432/{os.environ['POSTGRES_DB']}" +#c.JupyterHub.db_url = 'sqlite:///jupyterhub.sqlite' + + +# Authenticator configuration - NativeAuthenticator +c.JupyterHub.authenticator_class = NativeAuthenticator + +# Native Authenticator settings +#c.NativeAuthenticator.create_system_users = True +c.NativeAuthenticator.minimum_password_length = int(os.environ.get('NATIVE_AUTH_MIN_PASSWORD_LENGTH', '8')) +c.NativeAuthenticator.check_common_password = True +c.NativeAuthenticator.enable_signup = True +c.NativeAuthenticator.ask_email_on_signup = False +c.NativeAuthenticator.allow_2fa = False + +# Admin configuration +admin_user = os.environ.get('JUPYTERHUB_ADMIN_USER', 'admin') +c.Authenticator.admin_users = {admin_user, 'DerGrumpf'} +c.Authenticator.any_allow_config = True +c.Authenticator.allow_all = True + +# Custom logo and templates +c.JupyterHub.logo_file = '/srv/jupyterhub/templates/logo.png' +c.JupyterHub.template_paths = ['/etc/jupyterhub/templates'] + + +# Hub environment for containers +c.JupyterHub.spawner_class = DockerSpawner +c.Spawner.ip = '0.0.0.0' +c.DockerSpawner.image = os.environ.get('NOTEBOOK_IMAGE', 'jupyter/scipy-notebook:latest') +'''network_name = os.environ.get('DOCKER_NETWORK_NAME', 'stack_default') +c.DockerSpawner.network_name = network_name +c.DockerSpawner.extra_host_config = { + 'network_mode': network_name +} +c.DockerSpawner.remove = True +c.DockerSpawner.debug = True +c.DockerSpawner.default_url = os.environ.get('SPAWNER_DEFAULT_URL', '/lab') +c.DockerSpawner.use_internal_ip = True + +# Resources +c.DockerSpawner.mem_limit = os.environ.get('NOTEBOOK_MEMORY_LIMIT', '500M') +c.DockerSpawner.cpu_limit = float(os.environ.get('NOTEBOOK_CPU_LIMIT', '1.0')) + +# Volume +c.DockerSpawner.volumes = { + '/var/run/docker.sock': '/var/run/docker.sock' +# './data/jupyter/users/{username}': '/home/jovyan/work', +# './data/jupyter/nbgrader/exchange': '/srv/nbgrader/exchange', +# './data/jupyter/nbgrader/courses': '/srv/nbgrader/courses', +} + +c.DockerSpawner.notebook_dir = '/home/jovyan/work' + +# Container Environment +c.DockerSpawner.environment = { + 'GRANT_SUDO': '0', + 'JUPYTER_ENABLE_LAB': os.environ.get('ENABLE_LAB', '1'), + 'JUPYTERHUB_SINGLEUSER_APP': 'jupyter_server.serverapp.ServerApp' +} + +#c.DockerSpawner.hub_ip_connect = os.environ.get('HUB_IP', 'jupyterhub') +#c.DockerSpawner.hub_port_connect = 8081 + +def pre_spawn_hook(spawner): + """Create user directories before spawning""" + username = spawner.user.name + user_dir = f"./data/jupyter/users/{username}" + + import os + import stat + + if not os.path.exists(user_dir): + os.makedirs(user_dir, mode=0o755, exist_ok=True) + os.chown(user_dir, 1000, 1000) + + # TODO Nbgrader dirs + +c.DockerSpawner.pre_spawn_hook = pre_spawn_hook +''' + +''' +# Services configuration for NBGrader +c.JupyterHub.services = [ + { + 'name': 'nbgrader-formgrader', + 'url': 'http://127.0.0.1:9999', + 'command': [ + 'jupyter-nbgrader', + '--port=9999', + '--no-browser', + '--log-level=INFO' + ], + 'cwd': '/srv/jupyterhub/courses', + 'user': 'root', + 'environment': { + 'PYTHONPATH': '/srv/nbgrader', + 'NBGRADER_CONFIG_FILE': '/srv/jupyterhub/nbgrader/nbgrader_config.py' + } + } +] +''' +''' +# NBGrader configuration +c.JupyterHub.load_groups = { + 'nbgrader-instructors': ['instructor'], + 'nbgrader-students': [] +} +''' +''' +# NBGrader Config +c.JupyterHub.services = [ + { + 'name': 'nbgrader-formgrader', + 'url': 'http://127.0.0.1:9999', + 'api_token': generate_api_token(prefix='nbgrader'), + 'command': [ + 'python', '-m', 'nbgrader', 'formgrader', + #'--port=9999', + #'--no-browser', + '--log-level=INFO', + #'--base_url=/jupyter/services/nbgrader-formgrader', + '--debug' + ], + 'environment': { + 'JUPYTERHUB_SERVICE_URL': 'http://127.0.0.1:8081/jupyter/hub/api', + 'NBGRADER_CONFIG_FILE': '/srv/nbgrader/nbgrader_config.py', + 'PYTHONPATH': '/srv/nbgrader' + }, + 'cwd': '/srv/nbgrader/courses', + 'user': 'root' + } +] +''' + +# Security settings +c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/cookie_secret' +c.ConfigurableHTTPProxy.auth_token = os.environ.get('JUPYTERHUB_AUTH_TOKEN', '') + +# Logging +c.JupyterHub.log_level = os.environ.get('LOG_LEVEL', 'INFO') + +# Shutdown settings +c.JupyterHub.shutdown_on_logout = False +c.JupyterHub.cleanup_servers = True + +# Timeout settings +c.JupyterHub.active_server_limit = 0 +c.JupyterHub.concurrent_spawn_limit = 10 + +# Allow named servers +c.JupyterHub.allow_named_servers = True +c.JupyterHub.named_server_limit_per_user = 3 + +print("JupyterHub configuration loaded successfully!") +print(f"Base URL: {c.JupyterHub.base_url}") +print(f"Bind URL: {c.JupyterHub.bind_url}") +print(f"Database URL: {c.JupyterHub.db_url}") +print(f"Docker Network: {c.DockerSpawner.network_name}") +print(f"Docker Image: {c.DockerSpawner.image}") +print("Postgres available", is_service_available_cmd("postgres", 5432)) + +with open("/srv/jupyterhub/cookie_secret") as f: + print(f.readlines()) diff --git a/jupyterhub/nbgrader/nbgrader_config.py b/jupyterhub/nbgrader/nbgrader_config.py new file mode 100644 index 0000000..0e8e798 --- /dev/null +++ b/jupyterhub/nbgrader/nbgrader_config.py @@ -0,0 +1,30 @@ +# /srv/nbgrader/nbgrader_config.py +c = get_config() + +# 1. Course Configuration +c.CourseDirectory.course_id = "intro-to-python" # Change to your course name +c.CourseDirectory.root = '/srv/nbgrader/courses' + +# 2. Exchange Directory (for submitting/collecting assignments) +c.Exchange.root = '/srv/nbgrader/exchange' +c.Exchange.path_includes_course = True + +# 3. JupyterHub Integration +#c.NbGrader.hub_url = 'http://jupyterhub:8081/hub/api' +#c.NbGrader.hubapi_token = 'your-hub-token' # Generate with: openssl rand -hex 32 + +# 4. Database Configuration +c.StudentAssignmentNotebook.database_url = 'sqlite:////srv/nbgrader/nbgrader.sqlite' + +# 5. Assignment Policies +c.ExecutePreprocessor.timeout = 300 # 5 minutes timeout per cell +c.ClearSolutions.code_stub = "# YOUR CODE HERE" +c.ClearSolutions.text_stub = "YOUR ANSWER HERE" + +# 6. Formgrader UI Settings +c.FormgradeApp.port = 9999 +c.FormgradeApp.authenticator_class = 'nbgrader.auth.hubauth.HubAuth' +c.FormgradeApp.ip = '0.0.0.0' + +# 7. Permissions +c.CourseDirectory.groupshared = True diff --git a/jupyterhub/nbgrader_config.py b/jupyterhub/nbgrader_config.py new file mode 100644 index 0000000..0e8e798 --- /dev/null +++ b/jupyterhub/nbgrader_config.py @@ -0,0 +1,30 @@ +# /srv/nbgrader/nbgrader_config.py +c = get_config() + +# 1. Course Configuration +c.CourseDirectory.course_id = "intro-to-python" # Change to your course name +c.CourseDirectory.root = '/srv/nbgrader/courses' + +# 2. Exchange Directory (for submitting/collecting assignments) +c.Exchange.root = '/srv/nbgrader/exchange' +c.Exchange.path_includes_course = True + +# 3. JupyterHub Integration +#c.NbGrader.hub_url = 'http://jupyterhub:8081/hub/api' +#c.NbGrader.hubapi_token = 'your-hub-token' # Generate with: openssl rand -hex 32 + +# 4. Database Configuration +c.StudentAssignmentNotebook.database_url = 'sqlite:////srv/nbgrader/nbgrader.sqlite' + +# 5. Assignment Policies +c.ExecutePreprocessor.timeout = 300 # 5 minutes timeout per cell +c.ClearSolutions.code_stub = "# YOUR CODE HERE" +c.ClearSolutions.text_stub = "YOUR ANSWER HERE" + +# 6. Formgrader UI Settings +c.FormgradeApp.port = 9999 +c.FormgradeApp.authenticator_class = 'nbgrader.auth.hubauth.HubAuth' +c.FormgradeApp.ip = '0.0.0.0' + +# 7. Permissions +c.CourseDirectory.groupshared = True diff --git a/jupyterhub/templates/login.html b/jupyterhub/templates/login.html new file mode 100644 index 0000000..e044846 --- /dev/null +++ b/jupyterhub/templates/login.html @@ -0,0 +1,74 @@ +{% extends "templates/page.html" %} + +{% block login %} +
+
+
+ +
+
+
+{% endblock login %} + + diff --git a/jupyterhub/templates/logo.png b/jupyterhub/templates/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5d90a034d39f5cbededfa16018c1cb83c1cbb35a GIT binary patch literal 11257 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 V8C}yFJeUkXT3kV_T;yxO{{kMwH244j literal 0 HcmV?d00001 diff --git a/jupyterhub/templates/page.html b/jupyterhub/templates/page.html new file mode 100644 index 0000000..d312fae --- /dev/null +++ b/jupyterhub/templates/page.html @@ -0,0 +1,61 @@ + + + + + JupyterHub + + + + + + + + + + + + + + + + + + + + + + + + + + Hello + + diff --git a/jupyterhub/templates/signup.html b/jupyterhub/templates/signup.html new file mode 100644 index 0000000..499c6f6 --- /dev/null +++ b/jupyterhub/templates/signup.html @@ -0,0 +1,82 @@ +{% extends "templates/page.html" %} + +{% block login %} + +{% endblock login %} + + diff --git a/nginx/compose.yml b/nginx/compose.yml new file mode 100644 index 0000000..7ef741f --- /dev/null +++ b/nginx/compose.yml @@ -0,0 +1,15 @@ +services: + nginx: + image: nginx:alpine + container_name: nginx + restart: unless-stopped + ports: + - 8080:80 + - 8443:443 + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./conf.d:/etc/nginx/conf.d:ro + - ./snippets:/etc/nginx/snippets:ro + - ./ssl:/etc/nginx/ssl:ro + - ../logs/nginx:/etc/log/nginx + - ./discord.html:/var/www/static/index.html:ro diff --git a/nginx/conf.d/jupyterhub.conf b/nginx/conf.d/jupyterhub.conf new file mode 100644 index 0000000..343ed98 --- /dev/null +++ b/nginx/conf.d/jupyterhub.conf @@ -0,0 +1,43 @@ +# Shared proxy headers +include /etc/nginx/snippets/proxy-headers.conf; + +server { + listen 80 default_server; + server_name _; + + location /jupyter/ { + proxy_pass http://jupyterhub:8000/; # Docker service name + + # Apply shared proxy headers + include /etc/nginx/snippets/proxy-headers.conf; + + # Special WebSocket timeout + proxy_read_timeout 86400; + } + + # Redirect /jupyter to /jupyter/ + location = /jupyter { + return 302 /jupyter/; + } + + + # Stativ site conf + root /var/www/static; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 30d; + add_header Cache-Control "public, no-transform"; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + } + +} diff --git a/nginx/discord.html b/nginx/discord.html new file mode 100644 index 0000000..ea59dd8 --- /dev/null +++ b/nginx/discord.html @@ -0,0 +1,360 @@ + + + + + + Discord Mod Simulator 2025 + + + +
+

Discord Mod Simulator 2025

+
+
+ Members + 1000 +
+
+ Activity + 50% +
+
+ Reputation + 50% +
+
+ Day + 1 +
+
+ +
+ Welcome to Discord Mod Simulator 2025! As a moderator, you must balance keeping the server active while maintaining order. Make your decisions carefully! +
+ +
+ +
+ +
+
Game started. Good luck!
+
+
+ + + + diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..d026952 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,30 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml; + + # Include modular configs + include /etc/nginx/conf.d/*.conf; +} diff --git a/nginx/snippets/proxy-headers.conf b/nginx/snippets/proxy-headers.conf new file mode 100644 index 0000000..ffbd5be --- /dev/null +++ b/nginx/snippets/proxy-headers.conf @@ -0,0 +1,9 @@ +proxy_set_header Host $host; +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; + +# WebSocket support +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade"; diff --git a/postgres/compose.yml b/postgres/compose.yml new file mode 100644 index 0000000..aae7d72 --- /dev/null +++ b/postgres/compose.yml @@ -0,0 +1,22 @@ +services: + postgres: + image: postgres:15-alpine + container_name: postgres + environment: + POSTGRES_HOST: postgres + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_DB: ${POSTGRES_DB:-postgres} + ports: + - 5432:5432 + volumes: + - postgres_data:/var/lib/postgresql/data + restart: unless-stopped + #healthcheck: + # test: ["CMD-SHELL", "pg_isready -U postgres"] + # interval: 5s + # timeout: 5s + # retries: 5 + +volumes: + postgres_data: # Named volume for data persistence diff --git a/postgres/init-multiple-db.sh b/postgres/init-multiple-db.sh new file mode 100644 index 0000000..e69de29 diff --git a/postgres/postgresql.conf b/postgres/postgresql.conf new file mode 100644 index 0000000..c3c4d50 --- /dev/null +++ b/postgres/postgresql.conf @@ -0,0 +1,6 @@ +log_destination = 'stderr' +logging_collector = on +log_directory = '/var/log/postgres' +log_filename = 'postgresql-%Y-%m-%d.log' +log_statement = 'all' +log_rotation_age = 1d