- **Team**: Aron Petau & Friedrich Weber
- **Hours**: Tuesday - Thursday, 11:00 - 16:00
- **Location**: Room 124
diff --git a/backup.sh b/backup.sh
index 1aa0355..e2404b8 100755
--- a/backup.sh
+++ b/backup.sh
@@ -19,9 +19,9 @@ echo "Working directory: $SCRIPT_DIR"
# 1. Backup BookStack database
echo "Backing up BookStack database..."
docker exec bookstack-mariadb mariadb-dump \
- -u"${DB_USERNAME:-bookstack}" \
- -p"${DB_PASSWORD}" \
- "${DB_DATABASE:-bookstack}" \
+ -u"${BOOKSTACK_DB_USERNAME:-bookstack}" \
+ -p"${BOOKSTACK_DB_PASSWORD}" \
+ "${BOOKSTACK_DB_DATABASE:-bookstack}" \
| gzip > "$BACKUP_DIR/bookstack_db_$DATE.sql.gz"
# 2. Backup BookStack uploads and config
diff --git a/data/.DS_Store b/data/.DS_Store
index 645b942..53ad94d 100644
Binary files a/data/.DS_Store and b/data/.DS_Store differ
diff --git a/data/Tello_Drone_Setup.ipynb b/data/Tello_Drone_Setup.ipynb
new file mode 100644
index 0000000..fe66d06
--- /dev/null
+++ b/data/Tello_Drone_Setup.ipynb
@@ -0,0 +1,177 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "49a243e8",
+ "metadata": {},
+ "source": [
+ "# Tello Drone Programming - Setup\n",
+ "\n",
+ "Dieses Notebook hilft dir, die Programmierung des DJI Tello Drohne einzurichten.\n",
+ "\n",
+ "## Schritt 1: Pakete installieren\n",
+ "\n",
+ "Führe die folgende Zelle aus, um die benötigten Python-Pakete zu installieren:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4804023e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Installiere djitellopy und Abhängigkeiten\n",
+ "!pip install djitellopy opencv-python pillow"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a480f600",
+ "metadata": {},
+ "source": [
+ "## Schritt 2: Import und Verbindungstest\n",
+ "\n",
+ "Nach der Installation, importiere die Bibliothek:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7bcda678",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from djitellopy import Tello\n",
+ "import time\n",
+ "\n",
+ "print(\"✅ djitellopy erfolgreich importiert!\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "165d7797",
+ "metadata": {},
+ "source": [
+ "## Schritt 3: Drohne verbinden (nur ausführen wenn Drohne eingeschaltet ist!)\n",
+ "\n",
+ "**⚠️ WICHTIG:** Stelle sicher, dass:\n",
+ "1. Die Tello Drohne eingeschaltet ist\n",
+ "2. Dein Computer mit dem WiFi der Drohne verbunden ist (TELLO-XXXXXX)\n",
+ "3. Du genug Platz hast (mindestens 2x2 Meter freier Raum)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6acedf15",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Drohne initialisieren\n",
+ "drone = Tello()\n",
+ "\n",
+ "# Verbindung aufbauen\n",
+ "drone.connect()\n",
+ "\n",
+ "# Batterie-Status prüfen\n",
+ "battery = drone.get_battery()\n",
+ "print(f\"🔋 Batterie: {battery}%\")\n",
+ "\n",
+ "if battery < 20:\n",
+ " print(\"⚠️ Warnung: Batterie niedrig! Bitte aufladen.\")\n",
+ "else:\n",
+ " print(\"✅ Drohne bereit!\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c4c90848",
+ "metadata": {},
+ "source": [
+ "## Schritt 4: Einfacher Testflug\n",
+ "\n",
+ "**⚠️ SICHERHEIT ZUERST:**\n",
+ "- Freier Raum um dich herum\n",
+ "- Keine Personen in der Nähe\n",
+ "- Drohne auf ebener Fläche\n",
+ "- Bereit, die Drohne zu fangen falls nötig"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3ce22798",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Starten und einfache Bewegung\n",
+ "drone.takeoff() # Abheben\n",
+ "time.sleep(3) # 3 Sekunden schweben\n",
+ "\n",
+ "drone.move_up(30) # 30cm nach oben\n",
+ "time.sleep(2)\n",
+ "\n",
+ "drone.rotate_clockwise(90) # 90° drehen\n",
+ "time.sleep(2)\n",
+ "\n",
+ "drone.land() # Landen\n",
+ "print(\"✅ Testflug abgeschlossen!\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3b616df2",
+ "metadata": {},
+ "source": [
+ "## Nützliche Befehle\n",
+ "\n",
+ "Hier sind einige grundlegende Befehle:\n",
+ "\n",
+ "```python\n",
+ "# Bewegungen\n",
+ "drone.takeoff() # Abheben\n",
+ "drone.land() # Landen\n",
+ "drone.move_up(x) # x cm nach oben\n",
+ "drone.move_down(x) # x cm nach unten\n",
+ "drone.move_forward(x) # x cm vorwärts\n",
+ "drone.move_back(x) # x cm rückwärts\n",
+ "drone.move_left(x) # x cm links\n",
+ "drone.move_right(x) # x cm rechts\n",
+ "drone.rotate_clockwise(x) # x° im Uhrzeigersinn\n",
+ "drone.rotate_counter_clockwise(x) # x° gegen Uhrzeigersinn\n",
+ "\n",
+ "# Informationen\n",
+ "drone.get_battery() # Batterie-Status\n",
+ "drone.get_height() # Aktuelle Höhe\n",
+ "drone.get_speed() # Geschwindigkeit\n",
+ "\n",
+ "# Notfall\n",
+ "drone.emergency() # NOTLANDUNG (sofort stoppen!)\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "441ebe1d",
+ "metadata": {},
+ "source": [
+ "## Weiterführende Ressourcen\n",
+ "\n",
+ "- [DJITelloPy Dokumentation](https://djitellopy.readthedocs.io/)\n",
+ "- [Beispiele auf GitHub](https://github.com/damiafuentes/DJITelloPy/tree/master/examples)\n",
+ "\n",
+ "---\n",
+ "\n",
+ "**Viel Spaß beim Programmieren! 🚁**"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 231028c..d5fcf01 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -108,10 +108,10 @@ services:
image: lscr.io/linuxserver/bookstack:latest
container_name: bookstack
environment:
- - APP_KEY=${BOOKSTACK_APP_KEY}
- - APP_URL=${BOOKSTACK_APP_URL}
- env_file:
- - .env
+ - MYSQL_ROOT_PASSWORD=${BOOKSTACK_DB_PASSWORD}
+ - MYSQL_DATABASE=${BOOKSTACK_DB_DATABASE}
+ - MYSQL_USER=${BOOKSTACK_DB_USERNAME}
+ - MYSQL_PASSWORD=${BOOKSTACK_DB_PASSWORD}
volumes:
- ./bookstack/bookstack_app_data:/config
ports:
@@ -121,7 +121,11 @@ services:
bookstack-mariadb:
condition: service_healthy
healthcheck:
- test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]
+ test:
+ [
+ "CMD-SHELL",
+ "mariadb -u${BOOKSTACK_DB_USERNAME} -p${BOOKSTACK_DB_PASSWORD} -e 'SELECT 1' || exit 1",
+ ]
interval: 30s
timeout: 10s
retries: 3
@@ -135,10 +139,10 @@ services:
image: lscr.io/linuxserver/mariadb:latest
container_name: bookstack-mariadb
environment:
- - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
- - MYSQL_DATABASE=${DB_DATABASE}
- - MYSQL_USER=${DB_USERNAME}
- - MYSQL_PASSWORD=${DB_PASSWORD}
+ - MYSQL_ROOT_PASSWORD=${BOOKSTACK_DB_PASSWORD}
+ - MYSQL_DATABASE=${BOOKSTACK_DB_DATABASE}
+ - MYSQL_USER=${BOOKSTACK_DB_USERNAME}
+ - MYSQL_PASSWORD=${BOOKSTACK_DB_PASSWORD}
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
@@ -151,7 +155,7 @@ services:
test:
[
"CMD-SHELL",
- "mariadb -u${DB_USERNAME} -p${DB_PASSWORD} -e 'SELECT 1' || exit 1",
+ "mariadb -u${BOOKSTACK_DB_USERNAME} -p${BOOKSTACK_DB_PASSWORD} -e 'SELECT 1' || exit 1",
]
interval: 10s
timeout: 5s
@@ -207,6 +211,59 @@ services:
- "description=Portainer Container Management UI"
- "maintainer=Studio EinsZwoVier"
+ jupyterhub:
+ build: ./jupyterhub
+ container_name: jupyterhub
+ ports:
+ - "8001:8001"
+ - "8081:8081"
+ volumes:
+ - ./data:/home
+ - ./jupyterhub/jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro
+ - ./static/images/logo.png:/srv/jupyterhub/logo.png:ro
+ - /var/run/docker.sock:/var/run/docker.sock # Docker socket for spawning containers
+ env_file:
+ - .env
+ environment:
+ - JUPYTERHUB_SINGLEUSER_APP=jupyterlab
+ restart: unless-stopped
+ mem_limit: 2g
+ cpus: 1.0
+ depends_on:
+ - web
+ labels:
+ - "com.centurylinklabs.watchtower.enable=true"
+ - "description=JupyterHub for interactive notebooks"
+ - "maintainer=Studio EinsZwoVier"
+
+ forgejo:
+ image: codeberg.org/forgejo/forgejo:11
+ container_name: forgejo
+ environment:
+ - USER_UID=1000
+ - USER_GID=1000
+ - TZ=Europe/Berlin
+ - FORGEJO__repository__ENABLE_PUSH_CREATE_USER=true
+ restart: unless-stopped
+ ports:
+ - "${FORGEJO_PORT:-3003}:3000"
+ - "222:22"
+ volumes:
+ - ./forgejo/data:/data
+ mem_limit: 2g
+ cpus: 2.0
+ mem_reservation: 512m
+ healthcheck:
+ test: ["CMD-SHELL", "curl -f http://localhost:3000/ || exit 1"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+ labels:
+ - "com.centurylinklabs.watchtower.enable=true"
+ - "description=Forgejo Git Server"
+ - "maintainer=Studio EinsZwoVier"
+
volumes:
portainer_data:
driver: local
diff --git a/forgejo/data/git/.ssh/environment b/forgejo/data/git/.ssh/environment
new file mode 100644
index 0000000..f86169b
--- /dev/null
+++ b/forgejo/data/git/.ssh/environment
@@ -0,0 +1 @@
+GITEA_CUSTOM=/data/gitea
diff --git a/forgejo/data/gitea/conf/app.ini b/forgejo/data/gitea/conf/app.ini
new file mode 100644
index 0000000..afecad9
--- /dev/null
+++ b/forgejo/data/gitea/conf/app.ini
@@ -0,0 +1,63 @@
+APP_NAME = Forgejo: Beyond coding. We forge.
+RUN_MODE = prod
+
+[repository]
+ROOT = /data/git/repositories
+ENABLE_PUSH_CREATE_USER = true
+
+[repository.local]
+LOCAL_COPY_PATH = /data/gitea/tmp/local-repo
+
+[repository.upload]
+TEMP_PATH = /data/gitea/uploads
+
+[server]
+APP_DATA_PATH = /data/gitea
+DOMAIN = localhost
+SSH_DOMAIN = localhost
+HTTP_PORT = 3000
+ROOT_URL =
+DISABLE_SSH = false
+SSH_PORT = 22
+SSH_LISTEN_PORT = 22
+LFS_START_SERVER = false
+
+[database]
+PATH = /data/gitea/gitea.db
+DB_TYPE = sqlite3
+HOST = localhost:3306
+NAME = gitea
+USER = root
+PASSWD =
+LOG_SQL = false
+
+[indexer]
+ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve
+
+[session]
+PROVIDER_CONFIG = /data/gitea/sessions
+
+[picture]
+AVATAR_UPLOAD_PATH = /data/gitea/avatars
+REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars
+
+[attachment]
+PATH = /data/gitea/attachments
+
+[log]
+MODE = console
+LEVEL = info
+ROOT_PATH = /data/gitea/log
+
+[security]
+INSTALL_LOCK = false
+SECRET_KEY =
+REVERSE_PROXY_LIMIT = 1
+REVERSE_PROXY_TRUSTED_PROXIES = *
+
+[service]
+DISABLE_REGISTRATION = false
+REQUIRE_SIGNIN_VIEW = false
+
+[lfs]
+PATH = /data/git/lfs
diff --git a/forgejo/data/ssh/ssh_host_ecdsa_key b/forgejo/data/ssh/ssh_host_ecdsa_key
new file mode 100644
index 0000000..7e07159
--- /dev/null
+++ b/forgejo/data/ssh/ssh_host_ecdsa_key
@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQ02EGaaw86ZKkpWSL8gktKug0A/XNF
+TgcY469G/5ecX9H/AMxrPlOKCU0K+3eWGSCISQGlVLBRNpHK5VM9ga13AAAAsJrl+aya5f
+msAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDTYQZprDzpkqSlZ
+IvyCS0q6DQD9c0VOBxjjr0b/l5xf0f8AzGs+U4oJTQr7d5YZIIhJAaVUsFE2kcrlUz2BrX
+cAAAAhAPrnsLBtojpOaaAt0yDgy/lCpqRnfbGBkV5FMiFYvm+fAAAAEXJvb3RAMmQ4Yjk4
+Nzg0OWMyAQIDBAUG
+-----END OPENSSH PRIVATE KEY-----
diff --git a/forgejo/data/ssh/ssh_host_ecdsa_key.pub b/forgejo/data/ssh/ssh_host_ecdsa_key.pub
new file mode 100644
index 0000000..9e57846
--- /dev/null
+++ b/forgejo/data/ssh/ssh_host_ecdsa_key.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDTYQZprDzpkqSlZIvyCS0q6DQD9c0VOBxjjr0b/l5xf0f8AzGs+U4oJTQr7d5YZIIhJAaVUsFE2kcrlUz2BrXc= root@2d8b987849c2
diff --git a/forgejo/data/ssh/ssh_host_ed25519_key b/forgejo/data/ssh/ssh_host_ed25519_key
new file mode 100644
index 0000000..340221a
--- /dev/null
+++ b/forgejo/data/ssh/ssh_host_ed25519_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACD7RYGr6qRspA4rgtvxhXJiS4a7z/hXm8q2XS9J8iqncQAAAJit3hcbrd4X
+GwAAAAtzc2gtZWQyNTUxOQAAACD7RYGr6qRspA4rgtvxhXJiS4a7z/hXm8q2XS9J8iqncQ
+AAAEB2I1zsRzNSHt6lqkavsXdj1fi2cDcnSCImC4Kpaqi3APtFgavqpGykDiuC2/GFcmJL
+hrvP+FebyrZdL0nyKqdxAAAAEXJvb3RAMmQ4Yjk4Nzg0OWMyAQIDBA==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/forgejo/data/ssh/ssh_host_ed25519_key.pub b/forgejo/data/ssh/ssh_host_ed25519_key.pub
new file mode 100644
index 0000000..5a48f35
--- /dev/null
+++ b/forgejo/data/ssh/ssh_host_ed25519_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPtFgavqpGykDiuC2/GFcmJLhrvP+FebyrZdL0nyKqdx root@2d8b987849c2
diff --git a/forgejo/data/ssh/ssh_host_rsa_key b/forgejo/data/ssh/ssh_host_rsa_key
new file mode 100644
index 0000000..f60fa3a
--- /dev/null
+++ b/forgejo/data/ssh/ssh_host_rsa_key
@@ -0,0 +1,38 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+-----END OPENSSH PRIVATE KEY-----
diff --git a/forgejo/data/ssh/ssh_host_rsa_key.pub b/forgejo/data/ssh/ssh_host_rsa_key.pub
new file mode 100644
index 0000000..1442d1c
--- /dev/null
+++ b/forgejo/data/ssh/ssh_host_rsa_key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2FfioI52Oa6bL480x/ZFQiJqS4F2pBcxEh/OkozGyNIy7F/TpiyphP59FHoQ52JtTMwcmX0GoD8bPDYi26FIb2ugpW+TBtijJN+m3q03qrEnKxMHrMsZQoZT+bO05WAUvlpO2qO+i7f5QSfQEi+heXrG7WC2MYli3ZC3+5PEpK3ey9wbhc7nnoTtHZW1Tu/2cnqu2AkLHnkirnTNGDHM73adyESet+wps6sbyr7qO/ieOmwGoSIelc2Kk/zBQdzAAMvpbE8FyzrK5qcJaDBDx5grLJCT+ugtxIPtXRhcEf4LGQ/0/1ntBd19qID6Dai7yUS47C1mxE2IWTkb2Xu2tk6D0T1kh9vhjIVUD31qQotAzi+kHpuHz7tKLfIxRjvD2YUjiy93ygnFOxofTW3RO1irbOLLYc7WPDBp5Ee3IPZUDu363ciViuF0w1BJO/v3VkfIpfFwJW/dm3SAheW8UtOf5expa33TCSy3tSKENQdhp4fgKRXVzv6fV3xQP75s= root@2d8b987849c2
diff --git a/jupyterhub/Dockerfile b/jupyterhub/Dockerfile
new file mode 100644
index 0000000..08181ba
--- /dev/null
+++ b/jupyterhub/Dockerfile
@@ -0,0 +1,28 @@
+FROM python:3.13-slim
+
+# Install system deps
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ nodejs npm \
+ git \
+ sudo \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install JupyterHub, JupyterLab, notebook (required for singleuser), and DockerSpawner
+RUN pip install --no-cache-dir jupyterhub jupyterlab notebook dockerspawner
+
+# Install configurable-http-proxy (required by JupyterHub)
+RUN npm install -g configurable-http-proxy@^4.0.0
+
+# Ensure npm global binaries are available in PATH
+ENV PATH="/usr/local/bin:${PATH}"
+
+# Create directories
+RUN mkdir -p /srv/jupyterhub /srv/jupyterhub/data
+WORKDIR /srv/jupyterhub
+
+COPY jupyterhub_config.py /srv/jupyterhub/
+
+EXPOSE 8001
+
+CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"]
diff --git a/jupyterhub/jupyterhub_config.py b/jupyterhub/jupyterhub_config.py
new file mode 100644
index 0000000..d25ecc6
--- /dev/null
+++ b/jupyterhub/jupyterhub_config.py
@@ -0,0 +1,79 @@
+from traitlets.config import get_config
+import os
+
+# Minimal JupyterHub config for local multi-user usage inside docker-compose.
+# This config is intentionally small — customize for auth, spawners, and TLS in production.
+
+c = get_config()
+
+# Hub internal URL (where the hub process listens for /hub requests)
+c.JupyterHub.bind_url = 'http://:8081/hub/'
+
+# Hub IP for spawned containers to connect back (use container name on Docker network)
+c.JupyterHub.hub_connect_ip = 'jupyterhub'
+
+# Proxy configuration
+# - Proxy public address (what users access): default is port 8000, we want 8001
+# - Proxy API address (hub talks to proxy control): separate port 8002
+c.JupyterHub.port = 8001 # Public proxy port
+c.ConfigurableHTTPProxy.api_url = 'http://127.0.0.1:8002'
+
+# Lightweight built-in authenticator: no external package required. This is
+# intended only for local development/testing. It accepts any username as long
+# as the password equals the value of JUPYTER_PASSWORD (default: einszwo4).
+from jupyterhub.auth import Authenticator
+
+class SimpleEnvPasswordAuthenticator(Authenticator):
+ async def authenticate(self, handler, data):
+ """Authenticate any username when the shared env password matches.
+
+ Dev-only: returns the username on success, otherwise None.
+ """
+ username = data.get('username')
+ password = data.get('password')
+ if not username or not password:
+ return None
+ if password == os.environ.get('JUPYTER_PASSWORD', 'einszwo4'):
+ return username
+ return None
+
+# Use the in-file authenticator for dev/testing
+c.JupyterHub.authenticator_class = SimpleEnvPasswordAuthenticator
+
+# Allow any authenticated user to access (suppress the warning)
+c.Authenticator.allow_all = True
+
+# Use DockerSpawner for production-ready containerized user servers
+from dockerspawner import DockerSpawner
+c.JupyterHub.spawner_class = DockerSpawner
+
+# Docker image for single-user notebook servers
+c.DockerSpawner.image = 'jupyter/scipy-notebook:latest'
+
+# Connect to Docker socket
+c.DockerSpawner.use_internal_ip = True
+c.DockerSpawner.network_name = 'einszwovier_network'
+
+# Remove containers after they stop
+c.DockerSpawner.remove = True
+
+# Mount the data directory for persistent storage
+# Use the data directory we already have mounted in docker-compose
+c.DockerSpawner.volumes = {
+ 'jupyterhub-user-{username}': {'bind': '/home/jovyan/work', 'mode': 'rw'}
+}
+
+# Increase spawn timeout
+c.Spawner.start_timeout = 120
+c.Spawner.http_timeout = 120
+
+# Set notebook directory and default interface
+c.Spawner.notebook_dir = '/home/jovyan/work'
+c.Spawner.default_url = '/lab'
+
+# UI and data locations
+c.JupyterHub.logo_file = '/srv/jupyterhub/logo.png'
+c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/jupyterhub_cookie_secret'
+
+# Note: DockerSpawner creates isolated containers for each user.
+# Each user gets their own containerized Jupyter environment with persistent storage.
diff --git a/main.py b/main.py
index fa3327c..89a5b61 100644
--- a/main.py
+++ b/main.py
@@ -18,6 +18,7 @@ SERVER_HOSTNAME = os.environ.get("SERVER_HOSTNAME", "einszwovier.local")
BOOKSTACK_PORT = os.environ.get("BOOKSTACK_PORT", "6875")
OPENWEBUI_PORT = os.environ.get("OPENWEBUI_PORT", "8080")
PORTAINER_PORT = os.environ.get("PORTAINER_PORT", "9000")
+FORGEJO_PORT = os.environ.get("FORGEJO_PORT", "3003")
# Courses CSV path
COURSES_CSV = Path("data/courses.csv")
@@ -84,6 +85,7 @@ async def welcome(request: Request):
"bookstack_port": BOOKSTACK_PORT,
"openwebui_port": OPENWEBUI_PORT,
"portainer_port": PORTAINER_PORT,
+ "forgejo_port": FORGEJO_PORT,
},
)
diff --git a/restore.sh b/restore.sh
index a766bf5..5e230f1 100755
--- a/restore.sh
+++ b/restore.sh
@@ -34,9 +34,9 @@ sleep 10 # Wait for MariaDB to start
zcat "$BACKUP_DIR/bookstack_db_$DATE.sql.gz" | \
docker exec -i bookstack-mariadb mariadb \
- -u"${DB_USERNAME:-bookstack}" \
- -p"${DB_PASSWORD}" \
- "${DB_DATABASE:-bookstack}"
+ -u"${BOOKSTACK_DB_USERNAME:-bookstack}" \
+ -p"${BOOKSTACK_DB_PASSWORD}" \
+ "${BOOKSTACK_DB_DATABASE:-bookstack}"
# 2. Restore BookStack files
echo "Restoring BookStack files..."
diff --git a/templates/landing.html b/templates/landing.html
index 25f4275..43bdb87 100644
--- a/templates/landing.html
+++ b/templates/landing.html
@@ -60,6 +60,18 @@
Teste unsere schulischen Large Language Models direkt über die Weboberfläche.
+
+ JupyterHub
+ Interaktive Python-Notebooks für Datenanalyse, Visualisierung und kollaboratives
+ Programmieren. Schreib uns eine E-Mail für Login-Daten.
+
+
+
+ Forgejo Git Server
+ Versionskontrolle und Code-Hosting für kollaborative Softwareprojekte. Ideal für gemeinsame
+ Entwicklung und Projektmanagement.
+
+
Kontakt
Schreibe uns direkt an: einszwovier@gvb-gymnasium.de
@@ -70,7 +82,8 @@