---
tags: SPAWNER unter Docker
---
# DAS SPAWNER-PROJEKT V0.1
*27.01.2026 rwd*
---
## 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`