commit 406ed2c15852d74554a4c646faf7ec2f10407760 Author: XPS\Micro Date: Fri Jan 30 16:43:23 2026 +0100 Readme added diff --git a/README.md b/README.md new file mode 100644 index 0000000..750cda4 --- /dev/null +++ b/README.md @@ -0,0 +1,2599 @@ +--- +tags: SPAWNER unter Docker +--- + +# DAS SPAWNER-PROJEKT V0.1 +*27.01.2026 rwd* +![](https://hedgedoc.wieland.org/uploads/7aaace93-f7d9-4fe9-9553-94b2fa2e7031.png) + + +--- + +## Inhaltsverzeichnis + +1. [Projektübersicht](#projektübersicht) +2. [Architektur](#architektur) +3. [Voraussetzungen](#voraussetzungen) +4. [Installation](#installation) +5. [Konfiguration](#konfiguration) +6. [Dateistruktur](#dateistruktur) +7. [Komponenten im Detail](#komponenten-im-detail) +8. [Workflow](#workflow) +9. [Traefik-Integration](#traefik-integration) +10. [Sicherheit](#sicherheit) +11. [Deployment](#deployment) +12. [Troubleshooting](#troubleshooting) +13. [Erweiterungen](#erweiterungen) +14. [Best Practices](#best-practices) + +--- + +## Projektübersicht + +Der **Docker Container Spawner** ist eine leichtgewichtige Lösung zum automatischen Bereitstellen von isolierten Docker-Containern für einzelne Benutzer. Nach erfolgreicher Authentifizierung erhält jeder Benutzer einen eigenen Container mit einem dedizierten Webdienst, der über eine personalisierte Subdomain erreichbar ist. + +Das System basiert auf einer Flask-Architektur, die nach einer erfolgreichen Anmeldung automatisch dedizierte Container aus vordefinierten Vorlagen erstellt. Die Anbindung erfolgt über den **Reverse-Proxy Traefik**, der den Datenverkehr dynamisch über personalisierte Subdomains an die jeweiligen Dienste weiterleitet. Zu den Sicherheitsmerkmalen gehören strikte Ressourcenlimits für RAM und CPU sowie eine verschlüsselte Nutzerverwaltung via SQLite. Die Dokumentation beschreibt zudem umfassende Wartungsfunktionen wie das Lifecycle-Management von Containern und Best Practices für den produktiven Einsatz. + +Anwendungsgebiete finden sich vor allem in der Bereitstellung von +- Lernumgebungen +- Sandboxes +- SaaS-Plattformen + +--- + + +### Hauptfunktionen + +- **User-Management**: Registrierung und Login mit sicherer Passwort-Speicherung +- **Automatisches Container-Spawning**: Jeder User erhält einen eigenen Docker-Container +- **Dynamisches Routing**: Traefik routet automatisch zu den User-Containern +- **Resource-Management**: CPU- und RAM-Limits pro Container +- **Lifecycle-Management**: Starten, Stoppen und Neustarten von User-Containern +- **Template-basiert**: Neue User-Container aus vorgefertigten Images + +### Use Cases + +- **Entwicklungsumgebungen**: Isolierte Dev-Spaces für Entwickler +- **SaaS-Anwendungen**: Multi-Tenant-Webservices +- **Lernplattformen**: Übungsumgebungen für Schulungen +- **CI/CD-Pipelines**: On-Demand Build-Umgebungen +- **Sandbox-Umgebungen**: Sichere Test-Environments + +--- + +## Architektur + +### Komponenten-Übersicht + +```flow +st=>start: Browser +e=>end: End +op=>operation: Traefik +:80 / :443 +op2=>operation: Spawner Service +Flask + Docker SDK +:5000 +op3=>operation: Docker Daemon +op4=>operation: User Containers +USER-I | USER-II | USER-III | USER-nnn| + +st->op->op2->op3->op4 +``` + +### Datenfluss + +1. **Login**: User meldet sich über Web-UI an +2. **Authentication**: Flask validiert Credentials gegen SQLite-DB +3. **Container-Spawn**: Docker SDK startet neuen Container aus Template +4. **Label-Injection**: Traefik-Labels werden beim Container-Start gesetzt +5. **Auto-Discovery**: Traefik erkennt neuen Container und erstellt Route +6. **Redirect**: User wird zu persönlicher Subdomain weitergeleitet + +### Netzwerk-Architektur + +Alle Services laufen im gleichen Docker-Netzwerk (\`traefik-network\`), damit Traefik die User-Container erreichen kann: + +``` +traefik-network (bridge) +├── traefik (Reverse Proxy) +├── spawner (Management Service) +├── user-alice-1 (User Container) +├── user-bob-2 (User Container) +└── user-charlie-3 (User Container) +``` + +--- + +## Voraussetzungen + +### Hardware + +- **Min. 2 GB RAM**: Für Spawner + mehrere User-Container +- **Min. 20 GB Disk**: Für Images und Container-Volumes +- **Multi-Core CPU**: Empfohlen für parallele Container + +### Software + +- **Docker**: Version 20.10+ +- **Docker Compose**: Version 2.0+ +- **Python**: 3.11+ (im Container enthalten) +- **Traefik**: Version 2.x oder 3.x (optional, aber empfohlen) + +### Netzwerk + +- **Port 5000**: Spawner Web-UI (oder via Traefik) +- **Port 80/443**: Traefik (für User-Container-Routing) +- **Wildcard-DNS** oder \`/etc/hosts\`-Einträge für Subdomains + +--- + +## Installation + +### Schritt 1: Projekt-Setup + +```bash +# Repository erstellen +mkdir docker-spawner +cd docker-spawner + +# Verzeichnisstruktur anlegen +mkdir -p spawner/{templates,user-template,data} +cd spawner +``` + +### Schritt 2: Dateien erstellen + +Erstelle alle Dateien aus der Projektstruktur (siehe [Dateistruktur](#dateistruktur)). + +### Schritt 3: Traefik-Netzwerk erstellen + +```bash +docker network create traefik-network +``` + +### Schritt 4: User-Template-Image bauen + +```bash +cd user-template +docker build -t user-service-template:latest . +cd .. +``` + +### Schritt 5: Spawner starten + +```bash +docker-compose up -d --build +``` + +### Schritt 6: Traefik starten (falls noch nicht vorhanden) + +```bash +# Minimal Traefik docker-compose.yml +cat > traefik-compose.yml <100 User: Kubernetes mit Horizontal Pod Autoscaling empfohlen. + +### Kann ich verschiedene Services pro User bereitstellen? + +Ja! Erweitere das User-Modell um \`service_type\` und verwende verschiedene Template-Images. + +### Funktioniert das auch ohne Traefik? + +Ja! Alternativen: + +- **Nginx mit dynamischer Config-Generation** +- **HAProxy mit Runtime-API** +- **Caddy mit JSON-API** + +### Wie sichere ich den Docker-Socket ab? + +Verwende \`tecnativa/docker-socket-proxy\` mit eingeschränkten Permissions (siehe [Sicherheit](#sicherheit)). + +### Kann ich existierende User-Daten migrieren? + +Ja! Volume-Mounts verwenden und bei Migration Volumes kopieren: + +```bash +docker run --rm -v old-user-data:/from -v new-user-data:/to alpine sh -c "cp -av /from/* /to/" +``` + +--- + +## Ressourcen + +### Docker SDK Documentation + +- [Docker SDK for Python](https://docker-py.readthedocs.io/) +- [Docker Engine API](https://docs.docker.com/engine/api/) + +### Flask & Security + +- [Flask Documentation](https://flask.palletsprojects.com/) +- [Flask-Login](https://flask-login.readthedocs.io/) +- [OWASP Security Guidelines](https://owasp.org/www-project-web-security-testing-guide/) + +### Traefik + +- [Traefik Documentation](https://doc.traefik.io/traefik/) +- [Docker Provider](https://doc.traefik.io/traefik/providers/docker/) + +### Alternatives & Inspiration + +- [JupyterHub](https://github.com/jupyterhub/jupyterhub) +- [Code-Server](https://github.com/coder/code-server) +- [Gitpod](https://www.gitpod.io/) + +--- + +## Lizenz & Support + +Dieses Projekt ist ein Beispiel-Setup. Für Produktions-Einsatz: + +- **Security-Audit** durchführen +- **Load-Tests** mit erwarteter User-Anzahl +- **Backup-Strategie** implementieren +- **Monitoring** mit Prometheus/Grafana + +--- + +**Version**: 1.0.0 +**Erstellt**: Januar 2026 +**Zielgruppe**: DevOps, Platform Engineers, SaaS-Entwickler + + +--- + + +## Verzeichnisstruktur + +``` +spawner/ +├── Dockerfile +├── docker-compose.yml +├── requirements.txt +├── app.py +├── auth.py +├── container_manager.py +├── models.py +├── config.py +├── templates/ +│ ├── login.html +│ └── dashboard.html +└── user-template/ + └── Dockerfile +``` + +## Dockerfile (Spawner-Service) + +```dockerfile +FROM python:3.11-slim + +WORKDIR /app + +# System-Dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Python-Dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Application-Code +COPY . . + +# Daten-Verzeichnisse +RUN mkdir -p /app/data /app/logs && \ + chmod 755 /app/data /app/logs + +EXPOSE 5000 + +# Health-Check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5000/health || exit 1 + +CMD ["python", "app.py"] +``` + +## requirements.txt + +```txt +flask==3.0.0 +flask-login==0.6.3 +flask-sqlalchemy==3.1.1 +werkzeug==3.0.1 +docker==7.0.0 +PyJWT==2.8.0 +python-dotenv==1.0.0 +``` + +## config.py + +```python +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + # ======================================== + # Sicherheit + # ======================================== + SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') + + # Session-Sicherheit + SESSION_COOKIE_SECURE = os.getenv('BASE_DOMAIN', 'localhost') != 'localhost' + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = 'Lax' + PERMANENT_SESSION_LIFETIME = 3600 # 1 Stunde + + # ======================================== + # Datenbank + # ======================================== + SQLALCHEMY_DATABASE_URI = os.getenv( + 'DATABASE_URL', + 'sqlite:///data/users.db' + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # ======================================== + # Docker-Konfiguration + # ======================================== + DOCKER_SOCKET = os.getenv('DOCKER_SOCKET', 'unix://var/run/docker.sock') + USER_TEMPLATE_IMAGE = os.getenv('USER_TEMPLATE_IMAGE', 'user-service-template:latest') + + # ======================================== + # Traefik/Domain-Konfiguration + # ======================================== + BASE_DOMAIN = os.getenv('BASE_DOMAIN', 'localhost') + SPAWNER_SUBDOMAIN = os.getenv('SPAWNER_SUBDOMAIN', 'spawner') # ← FEHLTE! + TRAEFIK_NETWORK = os.getenv('TRAEFIK_NETWORK', 'web') + + # Vollständige Spawner-URL + SPAWNER_URL = f"{SPAWNER_SUBDOMAIN}.{BASE_DOMAIN}" + + # ======================================== + # Application-Settings + # ======================================== + # HTTPS automatisch für Nicht-Localhost + PREFERRED_URL_SCHEME = 'https' if BASE_DOMAIN != 'localhost' else 'http' + + # Spawner-Port (nur für Debugging wichtig) + SPAWNER_PORT = int(os.getenv('SPAWNER_PORT', 5000)) + + # ======================================== + # Optionale Einstellungen + # ======================================== + # Logging + LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + LOG_FILE = os.getenv('LOG_FILE', '/app/logs/spawner.log') + + # Container-Limits (für container_manager.py) + DEFAULT_MEMORY_LIMIT = os.getenv('DEFAULT_MEMORY_LIMIT', '512m') + DEFAULT_CPU_QUOTA = int(os.getenv('DEFAULT_CPU_QUOTA', 50000)) # 0.5 CPU + + # Container-Cleanup + CONTAINER_IDLE_TIMEOUT = int(os.getenv('CONTAINER_IDLE_TIMEOUT', 3600)) # 1h in Sekunden + + +class DevelopmentConfig(Config): + """Konfiguration für Entwicklung""" + DEBUG = True + TESTING = False + + +class ProductionConfig(Config): + """Konfiguration für Produktion""" + DEBUG = False + TESTING = False + + # Strikte Session-Sicherheit + SESSION_COOKIE_SECURE = True + + # Optional: PostgreSQL statt SQLite + # SQLALCHEMY_DATABASE_URI = os.getenv( + # 'DATABASE_URL', + # 'postgresql://spawner:password@postgres:5432/spawner' + # ) + + +class TestingConfig(Config): + """Konfiguration für Tests""" + TESTING = True + SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' + WTF_CSRF_ENABLED = False + + +# Config-Dict für einfaches Laden +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig, + 'default': DevelopmentConfig +} + +``` + +## models.py + +```python +from flask_sqlalchemy import SQLAlchemy +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from datetime import datetime + +db = SQLAlchemy() + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(200), nullable=False) + container_id = db.Column(db.String(100), nullable=True) + container_port = db.Column(db.Integer, nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) +``` + +## container_manager.py + +```python +import docker +from config import Config + +class ContainerManager: + def __init__(self): + self.client = docker.from_env() + + def spawn_container(self, user_id, username): + """Spawnt einen neuen Container für den User""" + try: + existing = self._get_user_container(username) + if existing and existing.status == 'running': + return existing.id, self._get_container_port(existing) + + # User-Container-Subdomain (OHNE spawner-subdomain!) + user_domain = f"{username}.{Config.BASE_DOMAIN}" + + container = self.client.containers.run( + Config.USER_TEMPLATE_IMAGE, + name=f"user-{username}-{user_id}", + detach=True, + network=Config.TRAEFIK_NETWORK, + labels={ + 'traefik.enable': 'true', + + # HTTP Router + f'traefik.http.routers.user{user_id}.rule': + f'Host(`{user_domain}`)', + f'traefik.http.routers.user{user_id}.entrypoints': 'web', + + # HTTPS Router (auskommentiert für initiale Tests) + # f'traefik.http.routers.user{user_id}-secure.rule': + # f'Host(`{user_domain}`)', + # f'traefik.http.routers.user{user_id}-secure.entrypoints': 'websecure', + # f'traefik.http.routers.user{user_id}-secure.tls.certresolver': 'hetzner', + + # Service + f'traefik.http.services.user{user_id}.loadbalancer.server.port': '8080', + + # Metadata + 'spawner.user_id': str(user_id), + 'spawner.username': username, + 'spawner.managed': 'true' + }, + environment={ + 'USER_ID': str(user_id), + 'USERNAME': username + }, + restart_policy={'Name': 'unless-stopped'}, + mem_limit=Config.DEFAULT_MEMORY_LIMIT, + cpu_quota=Config.DEFAULT_CPU_QUOTA + ) + + return container.id, 8080 + + except docker.errors.ImageNotFound: + raise Exception(f"Template-Image '{Config.USER_TEMPLATE_IMAGE}' nicht gefunden") + except docker.errors.APIError as e: + raise Exception(f"Docker API Fehler: {str(e)}") + + def stop_container(self, container_id): + """Stoppt einen User-Container""" + try: + container = self.client.containers.get(container_id) + container.stop(timeout=10) + return True + except docker.errors.NotFound: + return False + + def remove_container(self, container_id): + """Entfernt einen User-Container komplett""" + try: + container = self.client.containers.get(container_id) + container.remove(force=True) + return True + except docker.errors.NotFound: + return False + + def get_container_status(self, container_id): + """Gibt Status eines Containers zurück""" + try: + container = self.client.containers.get(container_id) + return container.status + except docker.errors.NotFound: + return 'not_found' + + def _get_user_container(self, username): + """Findet existierenden Container für User""" + filters = {'label': f'spawner.username={username}'} + containers = self.client.containers.list(all=True, filters=filters) + return containers[0] if containers else None + + def _get_container_port(self, container): + """Extrahiert Port aus Container-Config""" + return 8080 +``` + +## auth.py + +```python +from flask import Blueprint, render_template, redirect, url_for, request, flash +from flask_login import login_user, logout_user, login_required, current_user +from models import db, User +from container_manager import ContainerManager + +auth_bp = Blueprint('auth', __name__) +container_mgr = ContainerManager() + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + """User-Login""" + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password): + login_user(user) + + # Container spawnen wenn noch nicht vorhanden + if not user.container_id: + try: + container_id, port = container_mgr.spawn_container(user.id, user.username) + user.container_id = container_id + user.container_port = port + db.session.commit() + except Exception as e: + flash(f'Container-Start fehlgeschlagen: {str(e)}', 'error') + return redirect(url_for('auth.login')) + + flash('Login erfolgreich!', 'success') + return redirect(url_for('dashboard')) + else: + flash('Ungültige Anmeldedaten', 'error') + + return render_template('login.html') + +@auth_bp.route('/signup', methods=['GET', 'POST']) +def signup(): + """User-Registrierung""" + if request.method == 'POST': + username = request.form.get('username') + email = request.form.get('email') + password = request.form.get('password') + + # Prüfe ob User existiert + if User.query.filter_by(username=username).first(): + flash('Username bereits vergeben', 'error') + return redirect(url_for('auth.signup')) + + if User.query.filter_by(email=email).first(): + flash('Email bereits registriert', 'error') + return redirect(url_for('auth.signup')) + + # Neuen User anlegen + user = User(username=username, email=email) + user.set_password(password) + db.session.add(user) + db.session.commit() + + # Container aus Template bauen und starten + try: + container_id, port = container_mgr.spawn_container(user.id, user.username) + user.container_id = container_id + user.container_port = port + db.session.commit() + + flash('Registrierung erfolgreich! Container wird gestartet...', 'success') + login_user(user) + return redirect(url_for('dashboard')) + + except Exception as e: + db.session.delete(user) + db.session.commit() + flash(f'Registrierung fehlgeschlagen: {str(e)}', 'error') + + return render_template('signup.html') + +@auth_bp.route('/logout') +@login_required +def logout(): + """User-Logout""" + logout_user() + flash('Erfolgreich abgemeldet', 'success') + return redirect(url_for('auth.login')) +``` + +## app.py + +```python +from flask import Flask, render_template, redirect, url_for +from flask_login import LoginManager, login_required, current_user +from models import db, User +from auth import auth_bp +from config import Config +from container_manager import ContainerManager + +# Flask-App initialisieren +app = Flask(__name__) +app.config.from_object(Config) + +# Datenbank initialisieren +db.init_app(app) + +# Flask-Login initialisieren +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'auth.login' +login_manager.login_message = 'Bitte melde dich an, um auf diese Seite zuzugreifen.' +login_manager.login_message_category = 'error' + +# Blueprint registrieren +app.register_blueprint(auth_bp) + +@login_manager.user_loader +def load_user(user_id): + """Lädt User für Flask-Login""" + return User.query.get(int(user_id)) + +@app.route('/') +def index(): + """Startseite - Redirect zu Dashboard oder Login""" + if current_user.is_authenticated: + return redirect(url_for('dashboard')) + return redirect(url_for('auth.login')) + +@app.route('/dashboard') +@login_required +def dashboard(): + """Dashboard - zeigt Container-Status und Service-URL""" + container_mgr = ContainerManager() + container_status = 'unknown' + + if current_user.container_id: + container_status = container_mgr.get_container_status(current_user.container_id) + + # Service-URL für den User + scheme = app.config['PREFERRED_URL_SCHEME'] + service_url = f"{scheme}://{current_user.username}.{app.config['BASE_DOMAIN']}" + + return render_template('dashboard.html', + user=current_user, + service_url=service_url, + container_status=container_status) + +@app.route('/container/restart') +@login_required +def restart_container(): + """Startet Container des Users neu""" + container_mgr = ContainerManager() + + # Alten Container stoppen falls vorhanden + if current_user.container_id: + container_mgr.stop_container(current_user.container_id) + container_mgr.remove_container(current_user.container_id) + + # Neuen Container starten + try: + container_id, port = container_mgr.spawn_container(current_user.id, current_user.username) + current_user.container_id = container_id + current_user.container_port = port + db.session.commit() + except Exception as e: + app.logger.error(f"Container-Restart fehlgeschlagen: {str(e)}") + + return redirect(url_for('dashboard')) + +@app.route('/health') +def health(): + """Health-Check für Docker und Monitoring""" + try: + # DB-Check + db.session.execute('SELECT 1') + db_status = 'ok' + except Exception as e: + db_status = f'error: {str(e)}' + + try: + # Docker-Check + container_mgr = ContainerManager() + container_mgr.client.ping() + docker_status = 'ok' + except Exception as e: + docker_status = f'error: {str(e)}' + + status_code = 200 if db_status == 'ok' and docker_status == 'ok' else 503 + + return { + 'status': 'healthy' if status_code == 200 else 'unhealthy', + 'database': db_status, + 'docker': docker_status, + 'version': '1.0.0' + }, status_code + +# Datenbank-Tabellen erstellen beim ersten Start +with app.app_context(): + db.create_all() + app.logger.info('Datenbank-Tabellen erstellt') + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=False) +``` + +## docker-compose.yml + +```yaml +version: '3.8' + +services: + spawner: + build: . + container_name: spawner + restart: unless-stopped + + ports: + - "5000:5000" # Optional: Direktzugriff für Debugging + + volumes: + # Docker-Socket für Container-Management + - /var/run/docker.sock:/var/run/docker.sock:rw + # Persistente Daten + - ./data:/app/data + # Logs + - ./logs:/app/logs + + environment: + # Aus .env-Datei + - SECRET_KEY=${SECRET_KEY} + - BASE_DOMAIN=${BASE_DOMAIN} + - TRAEFIK_NETWORK=${TRAEFIK_NETWORK} + - USER_TEMPLATE_IMAGE=${USER_TEMPLATE_IMAGE:-user-service-template:latest} + - SPAWNER_SUBDOMAIN=${SPAWNER_SUBDOMAIN:-spawner} + + networks: + - web # ⚠️ Dein bestehendes Traefik-Netzwerk! + + labels: + # Traefik aktivieren + - "traefik.enable=true" + + # HTTP Router + - "traefik.http.routers.spawner.rule=Host(`${SPAWNER_SUBDOMAIN}.${BASE_DOMAIN}`)" + - "traefik.http.routers.spawner.entrypoints=web" + - "traefik.http.services.spawner.loadbalancer.server.port=5000" + + # Metadata für Management + - "spawner.managed=true" + - "spawner.version=1.0.0" + - "spawner.type=management-service" + + # Health-Check + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +# Externes Netzwerk (von deinem Traefik bereits vorhanden) +networks: + web: + external: true +``` + +## user-template/Dockerfile (Template für User-Container) + +```dockerfile +FROM nginxinc/nginx-unprivileged:alpine + +# Beispiel: Einfacher Webserver pro User +# HTML direkt in den Container schreiben +RUN echo '

Dein persönlicher Service

' > /usr/share/nginx/html/index.html + +EXPOSE 8080 + +CMD ["nginx", "-g", "daemon off;"] +``` + +**Hinweis**: Verwende `nginx-unprivileged` statt `nginx` für bessere Sicherheit (kein root-Prozess). +Container läuft auf Port 8080 (als unprivileged user). + +## templates/login.html + +```html + + + + Login - Spawner + + +

Login

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +

{{ message }}

+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ +
+

Noch kein Account? Registrieren

+ + +``` + +## templates/dashboard.html + +```html + + + + Dashboard - {{ user.username }} + + +

Willkommen, {{ user.username }}!

+ +

Container-Status: {{ container_status }}

+

Dein Service: {{ service_url }}

+ + Container neu starten
+ Logout + + +``` + +## Starten + +```bash +# Im spawner/ Verzeichnis: +docker-compose up --build +``` + +Die Lösung enthält: +- **Vollständige Authentifizierung** mit Flask-Login und gehashten Passwörtern +- **Automatisches Container-Spawning** via Docker SDK [docs.docker](https://docs.docker.com/reference/api/engine/sdk/examples/) +- **Traefik-Integration** über Labels [brunoscheufler](https://brunoscheufler.com/blog/2022-04-17-routing-traffic-for-dynamic-deployments-using-traefik) +- **Resource-Limits** (RAM/CPU) pro User-Container +- **Persistente Datenbank** für User-Management +- **Template-System** für neue User-Container + +Der Spawner-Service benötigt Zugriff auf `/var/run/docker.sock`, um Container zu steuern. + +--- + +# SPAWNER Integration in bestehende Traefik-Umgebung +## Step-by-Step Implementierungsplan + +--- + +## 🎯 Zielsetzung + +Integration des SPAWNER-Systems in eine bestehende Docker-Infrastruktur mit Traefik als Reverse Proxy, ohne bestehende Services zu beeinträchtigen. + +--- + +## 📋 Voraussetzungen prüfen + +### ✅ Checkliste vor Start + +- [ ] Traefik läuft und ist erreichbar +- [ ] Docker-Version ≥ 20.10 +- [ ] Freier Port für Spawner (Standard: 5000) +- [ ] Mindestens 2 GB freier RAM +- [ ] Wildcard-DNS oder manuelle DNS-Einträge möglich +- [ ] Zugriff auf `/var/run/docker.sock` +- [ ] Git installiert (zum Klonen/Erstellen der Dateien) + +--- + +## Phase 1: Analyse der bestehenden Umgebung + +### Step 1.1: Traefik-Konfiguration ermitteln + +**Aktion**: Bestehende Traefik-Setup analysieren + +```bash +# Traefik-Container finden +docker ps | grep traefik + +# Traefik-Konfiguration anzeigen +docker inspect | jq '.[0].Config.Labels' +docker inspect | jq '.[0].HostConfig.Binds' + +# Verwendetes Netzwerk ermitteln +docker inspect | jq '.[0].NetworkSettings.Networks' +``` + +**Dokumentieren**: +- Traefik-Version: _______________ +- Netzwerk-Name: _______________ +- EntryPoints: _______________ +- Zertifikats-Resolver (falls HTTPS): _______________ + +```bash=1 + docker ps | grep traefik +81e0f2d0f8c0 traefik:v3.6.5 +``` +```config=1 +docker inspect 81e0f2d0f8c0 | jq '.[0].Config.Labels' +{ + "com.docker.compose.config-hash": "aeb95d30dd87fd499dd7207ef416f97a6c325227615a2ccdae20278b5f70f51c", + "com.docker.compose.container-number": "1", + "com.docker.compose.depends_on": "", + "com.docker.compose.image": "sha256:0fb158a64eaac3b411525e180705dbb4e120d078150b6a795e120e6b80e81b02", + "com.docker.compose.oneoff": "False", + "com.docker.compose.project": "traefik", + "com.docker.compose.project.config_files": "/volume1/docker/traefik/docker-compose.yml", + "com.docker.compose.project.working_dir": "/volume1/docker/traefik", + "com.docker.compose.service": "traefik", + "com.docker.compose.version": "2.20.1", + "org.opencontainers.image.description": "A modern reverse-proxy", + "org.opencontainers.image.documentation": "https://docs.traefik.io", + "org.opencontainers.image.source": "https://github.com/traefik/traefik", + "org.opencontainers.image.title": "Traefik", + "org.opencontainers.image.url": "https://traefik.io", + "org.opencontainers.image.vendor": "Traefik Labs", + "org.opencontainers.image.version": "v3.6.5" +} +``` + +```config=1 +docker inspect 81e0f2d0f8c0 | jq '.[0].HostConfig.Binds' +[ + "/var/run/docker.sock:/var/run/docker.sock:rw", + "/volume1/docker/traefik/traefik.toml:/traefik.toml:rw", + "/volume1/docker/traefik/traefik_dynamic.toml:/traefik_dynamic.toml:rw", + "/volume1/docker/traefik/acme.json:/acme.json:rw" +] +``` + +```config=1 +docker inspect 81e0f2d0f8c0 | jq '.[0].NetworkSettings.Networks' +{ + "web": { + "IPAMConfig": null, + "Links": null, + "Aliases": [ + "traefik", + "traefik", + "81e0f2d0f8c0" + ], + "NetworkID": "79c8e53a1b0d38b655e769918c2ecfccf049461f0e1fe276362ccc1c13869aa3", + "EndpointID": "95e639cc48ced9bb06d58fd501bbf850bbe64e6050d5de75700ded13bdb1c4d4", + "Gateway": "192.168.16.1", + "IPAddress": "192.168.16.6", + "IPPrefixLen": 24, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:c0:a8:10:06", + "DriverOpts": null + } +} +``` + +### Step 1.2: Netzwerk-Topologie verstehen + +```bash +# Alle Docker-Netzwerke auflisten +docker network ls + +# Netzwerk-Details des Traefik-Netzwerks +docker network inspect + +# Welche Container sind bereits angeschlossen? +docker network inspect | jq '.[0].Containers' +``` + +```bash=1 +docker network ls +NETWORK ID NAME DRIVER SCOPE +37f47c9e1943 bridge bridge local +3ab114f137fa dokploy_dokploy_internal bridge local +04ae90e99953 host host local +208dfb8d38b0 jupyterhub bridge local +ed620451f21c none null local +79c8e53a1b0d web bridge local +``` + +**Entscheidung**: +- Existierendes Netzwerk nutzen: **Ja** ☐ / **Nein** ☐ +- Netzwerk-Name: `_______________` + +:::success +**Existierendes Netzwerk nutzen:** ✔ +**Netzwerk-Name: web** +::: +### Step 1.3: Domain-Strategie festlegen + +**Optionen**: + +**A) Subdomains pro User** (empfohlen) +``` +alice.spawner.example.com +bob.spawner.example.com +charlie.spawner.example.com +``` + +**B) Path-basiert** +``` +spawner.example.com/alice +spawner.example.com/bob +spawner.example.com/charlie +``` + + +**Gewählte Strategie**: ☐ A ☐ B +:::success +**B) Path-basiert** +::: +**Base-Domain**: `_______________` +:::success +**Base-Domain: coder.wieland.org** +::: +### Step 1.4: Traefik-Dashboard prüfen + +```bash +# Ist Dashboard aktiviert? +docker exec cat /etc/traefik/traefik.yml | grep -A5 "api:" + +# Dashboard-URL (Standard: Port 8080) +firefox http://:8080 +``` + +**Notiz**: Dashboard-URL für Monitoring: `_______________` + +--- + +## Phase 2: Projekt-Setup + +### Step 2.1: Projektverzeichnis erstellen + +```bash +# Zu deinem Docker-Projekten-Verzeichnis wechseln +cd /path/to/docker/projects # z.B. ~/docker oder /opt/docker + +# Spawner-Verzeichnis erstellen +mkdir -p spawner/{templates,user-template,data,logs} +cd spawner + +# Berechtigungen setzen +chmod 755 . +``` + +**Pfad dokumentieren**: `_______________` + +### Step 2.2: Core-Dateien erstellen + +```bash +# Python-Dateien +touch app.py auth.py container_manager.py models.py config.py + +# Docker-Dateien +touch Dockerfile docker-compose.yml .env .dockerignore + +# Templates +touch templates/login.html templates/signup.html templates/dashboard.html + +# User-Template +touch user-template/Dockerfile + +# README +touch README.md +``` + +### Step 2.3: .dockerignore erstellen + +```bash +cat > .dockerignore << 'EOF' +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.env +.venv +venv/ +data/*.db +logs/*.log +.git +.gitignore +.DS_Store +*.md +EOF +``` + +### Step 2.4: requirements.txt erstellen + +```bash +cat > requirements.txt << 'EOF' +flask==3.0.0 +flask-login==0.6.3 +flask-sqlalchemy==3.1.1 +werkzeug==3.0.1 +docker==7.0.0 +PyJWT==2.8.0 +python-dotenv==1.0.0 +EOF +``` + +--- + +## Phase 3: Anpassung an deine Umgebung + +### Step 3.1: .env-Datei konfigurieren + +Erstelle `.env` mit folgenden Variablen (anpassen!): + +``` +SECRET_KEY=ÄNDERE_MICH_ZU_RANDOM_STRING +BASE_DOMAIN=spawner.example.com +TRAEFIK_NETWORK=traefik-network +USER_TEMPLATE_IMAGE=user-service-template:latest +SPAWNER_PORT=5000 +``` + +SECRET_KEY generieren: +```bash +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +### Step 3.2: docker-compose.yml erstellen + +Vollständiges Beispiel mit Traefik-Labels - siehe Dokumentation. + +**Wichtige Anpassungen**: +- Netzwerk-Name +- EntryPoint-Name +- Domain-Name +- Port-Konflikte prüfen + +### Step 3.3: Traefik-Konfiguration erweitern + +Prüfe ob Docker-Provider aktiviert: + +```bash +docker exec cat /etc/traefik/traefik.yml +``` + +Falls Docker-Provider fehlt, ergänzen und Traefik neu starten. + +--- + +--- + +## Phase 4: User-Template vorbereiten + +### Step 4.1: Template-Dockerfile erstellen + +Im Verzeichnis `user-template/`: + +```dockerfile +FROM nginxinc/nginx-unprivileged:alpine + +# HTML kopieren UND Ownership setzen +COPY --chown=nginx:nginx index.html /usr/share/nginx/html/index.html + +EXPOSE 8080 +``` + +### Step 4.2: Beispiel index.html + +Erstelle eine einfache HTML-Seite für User-Container. + +### Step 4.3: Template-Image bauen + +```bash +cd user-template +docker build -t user-service-template:latest . + +# Test +docker run -d -p 8080:8080 --name test-user user-service-template:latest +curl http://localhost:8080 +docker stop test-user && docker rm test-user + +cd .. +``` + +--- + +## Phase 5: Spawner bauen und testen + +### Step 5.1: Alle Python-Dateien erstellen + +Kopiere die Code-Beispiele aus der Dokumentation: +- config.py +- models.py +- container_manager.py +- auth.py +- app.py + +### Step 5.2: Dockerfile erstellen + +```dockerfile +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN mkdir -p /app/data /app/logs && chmod 755 /app/data /app/logs + +EXPOSE 5000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5000/health || exit 1 + +CMD ["python", "app.py"] +``` + +### Step 5.3: Spawner bauen + +```bash +docker-compose build +``` + +### Step 5.4: Test-Start + +```bash +docker-compose up -d +docker-compose logs -f spawner + +# Health-Check +curl http://localhost:5000/health + +# Login-Seite +curl http://localhost:5000/login +``` + +### Step 5.5: Erste Test-Registrierung + +```bash +firefox http://localhost:5000 +``` + +Registriere einen Test-User und prüfe ob Container spawnt: + +```bash +docker ps | grep user- +docker inspect user-testuser-1 | jq '.[0].Config.Labels' +``` + +--- + +## Phase 6: Traefik-Integration aktivieren + +### Step 6.1: Netzwerk verbinden + +```bash +# Falls noch nicht verbunden +docker network connect traefik-network spawner + +# Verifizieren +docker network inspect traefik-network | grep spawner +``` + +### Step 6.2: DNS konfigurieren + +**Lokal testen** (Option A): +```bash +sudo nano /etc/hosts + +# Hinzufügen: +127.0.0.1 spawner.localhost +127.0.0.1 testuser.localhost +``` + +**Produktion** (Option B): +- Wildcard-DNS-Eintrag erstellen: `*.spawner.example.com → ` +- DNS-Propagation abwarten + +### Step 6.3: Traefik-Routing testen + +```bash +# Traefik-Dashboard öffnen +firefox http://:8080 + +# Routes prüfen unter HTTP → Routers + +# Mit curl testen +curl -H "Host: spawner.localhost" http://localhost/ +curl -H "Host: testuser.localhost" http://localhost/ +``` + +### Step 6.4: End-to-End Test + +1. Spawner-UI aufrufen +2. Neuen User registrieren +3. Zum Dashboard navigieren +4. Service-Link klicken → User-Container sollte erreichbar sein + +--- + +## Phase 7: HTTPS aktivieren (optional) + +### Step 7.1: Let's Encrypt konfigurieren + +Prüfe Traefik-Config für certificatesResolvers. + +### Step 7.2: Labels für HTTPS anpassen + +In `docker-compose.yml` und `container_manager.py` HTTPS-Labels ergänzen: +- entrypoints: websecure +- tls.certresolver: letsencrypt + +### Step 7.3: Spawner neu starten + +```bash +docker-compose down +docker-compose up -d --build +``` + +### Step 7.4: HTTPS testen + +```bash +firefox https://spawner.example.com + +# Zertifikat prüfen +openssl s_client -connect spawner.example.com:443 +``` + +--- + +## Phase 8: Monitoring & Observability + +### Step 8.1: Logging aktivieren + +Strukturiertes Logging in app.py implementieren. + +### Step 8.2: Monitoring-Script + +```bash +cat > monitor.sh << 'EOF' +#!/bin/bash +echo "=== SPAWNER Statistics ===" +docker stats spawner --no-stream +docker ps --filter "label=spawner.user_id" +docker stats $(docker ps --filter "label=spawner.user_id" -q) --no-stream +EOF + +chmod +x monitor.sh +./monitor.sh +``` + +### Step 8.3: Backup-Strategie + +```bash +cat > backup.sh << 'EOF' +#!/bin/bash +BACKUP_DIR="/backup/spawner" +DATE=$(date +%Y%m%d_%H%M%S) +mkdir -p $BACKUP_DIR +docker exec spawner sqlite3 /app/data/users.db ".backup '/app/data/backup_${DATE}.db'" +cp data/backup_${DATE}.db $BACKUP_DIR/ +find $BACKUP_DIR -name "backup_*.db" -mtime +7 -delete +EOF + +chmod +x backup.sh + +# Cronjob +crontab -e +# 0 2 * * * /path/to/spawner/backup.sh +``` + +--- + +## Phase 9: Produktions-Optimierung + +### Step 9.1: Ressourcen-Limits anpassen + +In `container_manager.py` basierend auf deiner Hardware. + +### Step 9.2: Container-Cleanup + +Script für automatisches Aufräumen alter Container. + +### Step 9.3: PostgreSQL statt SQLite + +Für Produktion docker-compose.yml um PostgreSQL erweitern. + +### Step 9.4: Rate-Limiting + +Flask-Limiter installieren und konfigurieren. + +--- + +## Phase 10: Go-Live + +### Step 10.1: Load-Test + +```bash +ab -n 1000 -c 10 http://spawner.example.com/login + +# Multi-User Test +for i in {1..10}; do + curl -X POST http://spawner.example.com/signup \ + -d "username=loadtest${i}&email=test${i}@example.com&password=test123" + sleep 2 +done +``` + +### Step 10.2: Security-Audit + +- SECRET_KEY stark +- HTTPS erzwungen +- Rate-Limiting aktiv +- Container-Isolation +- Non-Root-User + +```bash +# Vulnerability-Scan +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy image spawner:latest +``` + +### Step 10.3: Go-Live Checklist + +- [ ] Alle Services erreichbar +- [ ] HTTPS funktioniert +- [ ] DNS korrekt +- [ ] Backups laufen +- [ ] Monitoring aktiv +- [ ] Logs werden geschrieben +- [ ] Load-Tests bestanden +- [ ] Security-Audit durchgeführt +- [ ] Team informiert + +```bash +# Finaler Health-Check +curl -f https://spawner.example.com/health + +# Container-Count +docker ps --filter 'label=spawner.managed=true' -q | wc -l +``` + +**🎉 GO-LIVE!** + +--- + +## 🚨 Troubleshooting + +### Traefik findet Spawner nicht +- Netzwerk-Verbindung prüfen +- Labels verifizieren +- Traefik-Logs checken + +### User-Container startet nicht +- Template-Image existiert? +- Docker-Socket-Permissions +- Netzwerk vorhanden? + +### DNS funktioniert nicht +- Wildcard-DNS konfiguriert? +- /etc/hosts für lokale Tests +- DNS-Propagation abwarten + +### Container-Spawn schlägt fehl +- Docker-API-Zugriff testen +- Socket-Mount prüfen +- Permissions checken + +--- + +## 📊 Post-Integration + +### Wöchentliches Monitoring + +```bash +# Container-Anzahl +docker ps --filter "label=spawner.managed=true" + +# Ressourcen +docker stats --no-stream + +# Disk-Space +docker system df +``` + +### Metriken + +- Anzahl User +- Aktive Container +- CPU/RAM-Auslastung +- Netzwerk-Traffic + +--- + +## 🎓 Next Steps + +1. User-Feedback sammeln +2. Templates erweitern (Python, Node.js) +3. Admin-Dashboard entwickeln +4. Auto-Shutdown implementieren +5. Volume-Persistenz aktivieren +6. Multi-Region deployment + +--- + +**Integration abgeschlossen!** 🚀 + +Bei Fragen: +- Logs: `docker-compose logs -f` +- Traefik: `http://:8080` +- Health: `curl https://spawner.example.com/health`