--- 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`