62 KiB
| tags |
|---|
| SPAWNER unter Docker |
DAS SPAWNER-PROJEKT V0.1
Inhaltsverzeichnis
- Projektübersicht
- Architektur
- Voraussetzungen
- Installation
- Konfiguration
- Dateistruktur
- Komponenten im Detail
- Workflow
- Traefik-Integration
- Sicherheit
- Deployment
- Troubleshooting
- Erweiterungen
- 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
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
- Login: User meldet sich über Web-UI an
- Authentication: Flask validiert Credentials gegen SQLite-DB
- Container-Spawn: Docker SDK startet neuen Container aus Template
- Label-Injection: Traefik-Labels werden beim Container-Start gesetzt
- Auto-Discovery: Traefik erkennt neuen Container und erstellt Route
- 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
# 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).
Schritt 3: Traefik-Netzwerk erstellen
docker network create traefik-network
Schritt 4: User-Template-Image bauen
cd user-template
docker build -t user-service-template:latest .
cd ..
Schritt 5: Spawner starten
docker-compose up -d --build
Schritt 6: Traefik starten (falls noch nicht vorhanden)
# Minimal Traefik docker-compose.yml
cat > traefik-compose.yml <<EOF
version: '3.8'
services:
traefik:
image: traefik:v3.0
ports:
- "80:80"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
command:
- --api.insecure=true
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
networks:
- traefik-network
networks:
traefik-network:
external: true
EOF
docker-compose -f traefik-compose.yml up -d
Schritt 7: DNS konfigurieren (lokal)
# Für lokale Tests: /etc/hosts bearbeiten
sudo nano /etc/hosts
# Hinzufügen:
127.0.0.1 spawner.localhost
127.0.0.1 testuser.localhost
127.0.0.1 alice.localhost
Schritt 8: Zugriff testen
Browser öffnen: `http://spawner.localhost` (oder `http://localhost:5000`)
Konfiguration
Environment-Variablen
Die Konfiguration erfolgt über `.env` oder `docker-compose.yml`:
| Variable | Default | Beschreibung |
|---|---|---|
| `SECRET_KEY` | `dev-secret-key` | Flask Session Secret (ÄNDERN in Produktion!) |
| `BASE_DOMAIN` | `localhost` | Domain für User-Subdomains |
| `TRAEFIK_NETWORK` | `traefik-network` | Docker-Netzwerk für Traefik |
| `USER_TEMPLATE_IMAGE` | `user-service-template:latest` | Docker-Image für User-Container |
| `DOCKER_SOCKET` | `unix://var/run/docker.sock` | Docker-API-Socket |
.env Beispiel
SECRET_KEY=supersecret123changeme
BASE_DOMAIN=example.com
TRAEFIK_NETWORK=traefik-network
USER_TEMPLATE_IMAGE=user-service-template:latest
Produktions-Konfiguration
In `config.py` anpassen:
class ProductionConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = 'postgresql://user:pass@db:5432/spawner'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
Dateistruktur
spawner/
├── Dockerfile # Container für Spawner-Service
├── docker-compose.yml # Orchestrierung
├── requirements.txt # Python-Dependencies
├── .env # Environment-Variablen (nicht committen!)
│
├── app.py # Flask-Hauptanwendung
├── auth.py # Authentifizierungs-Blueprint
├── container_manager.py # Docker-Container-Management
├── models.py # SQLAlchemy User-Modell
├── config.py # Konfigurationsklassen
│
├── templates/ # Jinja2-Templates
│ ├── login.html
│ ├── signup.html
│ └── dashboard.html
│
├── user-template/ # Template für User-Container
│ └── Dockerfile
│
├── data/ # Persistente Daten
│ └── users.db # SQLite-Datenbank (auto-generiert)
│
└── logs/ # Logs (optional)
└── spawner.log
Komponenten im Detail
1. app.py - Flask-Hauptanwendung
Funktion: Einstiegspunkt, registriert Blueprints, initialisiert Extensions
Wichtige Routen:
- `/`: Redirect zu Dashboard oder Login
- `/dashboard`: Zeigt Container-Status und Service-URL
- `/container/restart`: Neustart des User-Containers
Flask-Extensions:
- `Flask-Login`: Session-Management
- `Flask-SQLAlchemy`: Datenbank-ORM
- `Flask-WTF`: CSRF-Protection (optional)
2. auth.py - Authentifizierung
Routes:
- `/login` (GET/POST): User-Login
- `/signup` (GET/POST): Neue User-Registrierung
- `/logout`: Session beenden
Workflow bei Login:
- Credentials validieren gegen DB
- Flask-Login Session erstellen
- Container spawnen falls noch nicht vorhanden
- Redirect zu Dashboard
Workflow bei Signup:
- Username/Email-Uniqueness prüfen
- Passwort hashen (bcrypt via werkzeug)
- User in DB speichern
- Container aus Template spawnen
- Container-ID in User-Record speichern
- Auto-Login
3. container_manager.py - Docker-Management
Klasse: `ContainerManager`
Methoden:
spawn_container(user_id, username)
# Startet neuen Container mit Traefik-Labels
# Returns: (container_id, port)
stop_container(container_id)
# Stoppt Container graceful (10s timeout)
remove_container(container_id)
# Entfernt Container komplett
get_container_status(container_id)
# Returns: 'running' | 'exited' | 'not_found'
build_template_for_user(username)
# Baut user-spezifisches Image (optional)
Docker-SDK Features:
- `from_env()`: Automatische Socket-Erkennung
- `containers.run()`: Container starten
- `labels={}`: Traefik-Routing-Config
- `mem_limit`, `cpu_quota`: Resource-Limits
- `restart_policy`: Auto-Restart bei Absturz
4. models.py - Datenbank-Schema
User-Modell:
class User:
id: int # Primary Key
username: str # Unique
email: str # Unique
password_hash: str # bcrypt Hash
container_id: str # Docker Container ID
container_port: int # Service-Port im Container
created_at: datetime # Registration Timestamp
Methoden:
- `set_password(password)`: Hash-Generierung
- `check_password(password)`: Validierung
- `UserMixin`: Flask-Login Integration
5. config.py - Zentrale Konfiguration
Config-Klasse:
Enthält alle konfigurierbaren Parameter:
- Datenbank-URI
- Docker-Socket-Pfad
- Template-Image-Name
- Domain-Konfiguration
- Netzwerk-Settings
Verwendung:
from config import Config
app.config.from_object(Config)
6. templates/ - Web-UI
login.html:
- Einfaches Login-Formular
- Flash-Messages für Fehler
- Link zu Signup
signup.html:
- Registrierungsformular
- Username, Email, Password
- Validation-Hinweise
dashboard.html:
- Container-Status-Anzeige
- Link zum User-Service
- Container-Restart-Button
- Logout-Link
Workflow
User-Registrierung
1. User öffnet /signup
2. Füllt Formular aus (Username, Email, Passwort)
3. POST zu /signup
4. Backend:
a. Validiert Input
b. Prüft auf Duplikate
c. Erstellt User-Record mit Hash
d. Spawnt Container aus Template
e. Speichert Container-ID
f. Loggt User automatisch ein
5. Redirect zu /dashboard
6. User sieht Link zu seinem Service
User-Login (existing user)
1. User öffnet /login
2. Gibt Credentials ein
3. POST zu /login
4. Backend:
a. Findet User in DB
b. Validiert Passwort-Hash
c. Prüft ob Container existiert
d. Falls nein: Spawnt neuen Container
e. Flask-Login Session erstellen
5. Redirect zu /dashboard
6. User klickt auf Service-URL
7. Traefik routet zu User-Container
Container-Lifecycle
[User Login]
│
▼
[Container existiert?]
│
├─ Ja ──► [Status prüfen] ──► [Running?]
│ │
│ ├─ Ja ──► [Redirect zu Service]
│ └─ Nein ─► [Container starten]
│
└─ Nein ─► [spawn_container()]
│
├─ Image pullen
├─ Container erstellen
├─ Labels setzen (Traefik)
├─ Netzwerk verbinden
├─ Resource-Limits setzen
└─ Container starten
│
▼
[Container läuft]
│
▼
[Traefik entdeckt Container]
│
▼
[Route wird erstellt]
│
▼
[Service erreichbar unter subdomain]
Traefik-Integration
Label-basiertes Routing
Bei jedem Container-Start werden Traefik-Labels gesetzt:
labels={
# Container für Traefik sichtbar machen
'traefik.enable': 'true',
# Router-Definition
'traefik.http.routers.user123.rule': 'Host(\`alice.example.com\`)',
'traefik.http.routers.user123.entrypoints': 'web',
# Service-Definition (Backend)
'traefik.http.services.user123.loadbalancer.server.port': '8080',
# Custom Labels für Spawner
'spawner.user_id': '123',
'spawner.username': 'alice'
}
Traefik-Konfiguration
Minimal `traefik.yml`:
entryPoints:
web:
address: ":80"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false # Nur Container mit traefik.enable=true
network: traefik-network
api:
insecure: true # Dashboard auf :8080 (nur dev!)
Automatische Service-Discovery
Traefik überwacht Docker-Events:
1. Container startet mit Labels
2. Traefik empfängt Docker-Event
3. Traefik parsed Labels
4. Router + Service werden erstellt
5. Route ist sofort aktiv (< 1 Sekunde)
HTTPS mit Let's Encrypt (optional)
Für Produktion in `traefik.yml` ergänzen:
entryPoints:
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
Labels anpassen:
'traefik.http.routers.user123.entrypoints': 'websecure',
'traefik.http.routers.user123.tls.certresolver': 'letsencrypt'
Sicherheit
Passwort-Sicherheit
- Hashing: Werkzeug's `generate_password_hash()` (pbkdf2:sha256)
- Salt: Automatisch pro Passwort
- Keine Plaintext-Speicherung: Nur Hashes in DB
Container-Isolation
- User-Namespaces: Jeder Container läuft mit eigenem UID-Mapping
- Resource-Limits: CPU/RAM-Beschränkung verhindert Denial-of-Service
- Network-Isolation: Container können sich gegenseitig nicht erreichen (außer via Traefik)
- Read-only Filesystem (optional):
container = client.containers.run(
read_only=True,
tmpfs={'/tmp': 'size=100M'}
)
Docker-Socket-Sicherheit
Problem: Spawner benötigt Zugriff auf `/var/run/docker.sock` (Root-Privilegien!)
Risiken:
- Container kann alle anderen Container kontrollieren
- Potenzieller Container-Escape
Mitigations:
- Docker-Socket-Proxy (empfohlen für Produktion):
services:
docker-proxy:
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
CONTAINERS: 1
NETWORKS: 1
SERVICES: 0
SWARM: 0
VOLUMES: 0
spawner:
environment:
DOCKER_HOST: tcp://docker-proxy:2375
-
Least Privilege: Nur notwendige Docker-API-Calls erlauben
-
Audit-Logging: Alle Container-Operationen loggen
Session-Sicherheit
# In config.py
SESSION_COOKIE_SECURE = True # Nur HTTPS
SESSION_COOKIE_HTTPONLY = True # Kein JS-Zugriff
SESSION_COOKIE_SAMESITE = 'Lax' # CSRF-Schutz
PERMANENT_SESSION_LIFETIME = 3600 # 1h Timeout
Input-Validation
# Username: Nur alphanumerisch + underscore
import re
if not re.match(r'^[a-zA-Z0-9_]{3,20}$', username):
raise ValueError("Invalid username")
# Container-Name-Injection verhindern
container_name = f"user-{username}-{user_id}".replace('/', '-')
Secrets-Management
Für Produktion: Keine Secrets in `docker-compose.yml`!
# Docker Secrets verwenden
echo "supersecretkey" | docker secret create flask_secret -
# In compose:
secrets:
- flask_secret
services:
spawner:
secrets:
- flask_secret
Deployment
Entwicklung
# Mit Auto-Reload
docker-compose up
# Logs verfolgen
docker-compose logs -f spawner
Staging
# .env.staging erstellen
cp .env .env.staging
# Mit staging-Config starten
docker-compose --env-file .env.staging up -d
# Health-Check
curl http://spawner.staging.example.com/health
Produktion
# Produktions-Image bauen
docker build -t spawner:1.0.0 .
# Mit PostgreSQL statt SQLite
docker-compose -f docker-compose.prod.yml up -d
# Monitoring einbinden
docker-compose -f docker-compose.prod.yml -f monitoring.yml up -d
Multi-Host Deployment (Docker Swarm)
# docker-compose.swarm.yml
version: '3.8'
services:
spawner:
image: spawner:1.0.0
deploy:
replicas: 3
placement:
constraints:
- node.role == manager # Wegen Docker-Socket
restart_policy:
condition: on-failure
docker stack deploy -c docker-compose.swarm.yml spawner-stack
Kubernetes (fortgeschritten)
Für K8s benötigst du:
- Docker-in-Docker (DinD) oder
- Kubernetes-API statt Docker-SDK
- Custom Spawner-Logic für Pods statt Container
Beispiel: JupyterHub's KubeSpawner als Referenz.
Troubleshooting
Container startet nicht
Symptom: `spawn_container()` wirft Exception
Debug:
# Spawner-Logs prüfen
docker logs spawner
# Docker-Events live verfolgen
docker events
# Manuell Container starten (Test)
docker run --rm -it user-service-template:latest sh
Häufige Ursachen:
- Image nicht gefunden: `docker images | grep user-service-template`
- Netzwerk existiert nicht: `docker network ls`
- Port-Konflikt: Ports bereits belegt
- Keine Docker-Socket-Berechtigung: Volume-Mount prüfen
Traefik routet nicht
Symptom: 404 bei `http://alice.localhost`
Debug:
# Traefik-Dashboard öffnen
firefox http://localhost:8080
# Unter "HTTP Routers" prüfen ob user-Route existiert
# Container-Labels prüfen
docker inspect user-alice-1 | jq '.[0].Config.Labels'
# Netzwerk prüfen
docker network inspect traefik-network
Fixes:
- Container läuft nicht im richtigen Netzwerk: `network` in `spawn_container()` prüfen
- Labels falsch: Syntax in `container_manager.py` korrigieren
- DNS-Problem: `/etc/hosts` prüfen
Datenbank-Fehler
Symptom: `OperationalError: no such table: user`
Fix:
# In Container einsteigen
docker exec -it spawner bash
# Python-Shell öffnen
python
# DB initialisieren
from app import app, db
with app.app_context():
db.create_all()
Performance-Probleme
Symptom: System langsam bei vielen Usern
Analyse:
# Container-Stats
docker stats
# Ressourcen pro Container
docker ps -q | xargs docker inspect | jq '.[].HostConfig.Memory'
Optimierungen:
- Resource-Limits anpassen
- Container automatisch stoppen nach Inaktivität
- Shared Volumes statt Copy-on-Write
- Redis für Session-Storage
Memory-Leak
Symptom: Spawner-Container wächst stetig
Debug:
# Memory-Profiling aktivieren
import tracemalloc
tracemalloc.start()
# In app.py nach Requests:
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
Häufige Ursache: Docker-Client-Objekte nicht geschlossen
Fix:
# context manager verwenden
with docker.from_env() as client:
client.containers.run(...)
Erweiterungen
1. Container-Timeout (Auto-Shutdown)
Ziel: Container nach 1h Inaktivität stoppen
# In models.py
class User(db.Model):
last_activity = db.Column(db.DateTime, default=datetime.utcnow)
# Cronjob in app.py
from apscheduler.schedulers.background import BackgroundScheduler
def cleanup_inactive_containers():
timeout = timedelta(hours=1)
inactive_users = User.query.filter(
User.last_activity < datetime.utcnow() - timeout
).all()
for user in inactive_users:
if user.container_id:
container_mgr.stop_container(user.container_id)
scheduler = BackgroundScheduler()
scheduler.add_job(cleanup_inactive_containers, 'interval', minutes=15)
scheduler.start()
2. Volume-Persistenz
Ziel: User-Daten überleben Container-Neustarts
# In spawn_container()
volumes = {
f'/data/users/{username}': {
'bind': '/app/data',
'mode': 'rw'
}
}
container = client.containers.run(
volumes=volumes,
# ...
)
3. Resource-Quotas pro User
# In models.py
class User(db.Model):
cpu_quota = db.Column(db.Integer, default=50000) # 0.5 CPU
memory_limit = db.Column(db.String, default='512m')
# In spawn_container()
container = client.containers.run(
cpu_quota=user.cpu_quota,
mem_limit=user.memory_limit,
# ...
)
4. Multi-Template-Support
# In models.py
class User(db.Model):
template_type = db.Column(db.String, default='basic')
TEMPLATES = {
'basic': 'user-service-template:latest',
'python': 'user-python-env:latest',
'node': 'user-node-env:latest'
}
# In spawn_container()
image = TEMPLATES.get(user.template_type, TEMPLATES['basic'])
5. WebSocket-Support für Logs
from flask_socketio import SocketIO, emit
socketio = SocketIO(app)
@socketio.on('stream_logs')
def stream_container_logs(container_id):
container = client.containers.get(container_id)
for line in container.logs(stream=True):
emit('log_line', {'data': line.decode()})
6. Admin-Dashboard
# In models.py
class User(db.Model):
is_admin = db.Column(db.Boolean, default=False)
# Neue Route in app.py
@app.route('/admin')
@login_required
def admin_dashboard():
if not current_user.is_admin:
abort(403)
users = User.query.all()
containers = client.containers.list(all=True)
return render_template('admin.html', users=users, containers=containers)
7. API-Endpoints
# RESTful API für externe Integration
@app.route('/api/container/start', methods=['POST'])
@api_key_required
def api_start_container():
data = request.json
user = User.query.get(data['user_id'])
container_id, port = container_mgr.spawn_container(user.id, user.username)
return jsonify({
'container_id': container_id,
'url': f'http://{user.username}.{Config.BASE_DOMAIN}'
})
8. Metrics & Monitoring
from prometheus_flask_exporter import PrometheusMetrics
metrics = PrometheusMetrics(app)
# Custom Metrics
active_containers = Gauge('spawner_active_containers', 'Number of running user containers')
@metrics.counter('spawner_logins', 'Login attempts')
def login():
# ...
Best Practices
1. Container-Images optimieren
# Multi-stage Build
FROM python:3.11 AS builder
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.11-slim
COPY --from=builder /wheels /wheels
RUN pip install --no-cache /wheels/*
# Non-root User
RUN useradd -m appuser
USER appuser
2. Health-Checks implementieren
# In app.py
@app.route('/health')
def health():
# DB-Check
try:
db.session.execute('SELECT 1')
except:
return jsonify({'status': 'unhealthy', 'db': 'down'}), 503
# Docker-Check
try:
client.ping()
except:
return jsonify({'status': 'unhealthy', 'docker': 'down'}), 503
return jsonify({'status': 'healthy'})
# In docker-compose.yml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
3. Logging strukturieren
import logging
import json
class JsonFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
'timestamp': self.formatTime(record),
'level': record.levelname,
'message': record.getMessage(),
'module': record.module
})
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
app.logger.addHandler(handler)
4. Graceful Shutdown
import signal
import sys
def cleanup(signum, frame):
print("Shutting down gracefully...")
# Alle aktiven Container stoppen
containers = client.containers.list(filters={'label': 'spawner.managed=true'})
for container in containers:
container.stop(timeout=30)
sys.exit(0)
signal.signal(signal.SIGTERM, cleanup)
signal.signal(signal.SIGINT, cleanup)
5. Rate-Limiting
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
# ...
6. Backups
# Cronjob für DB-Backup
0 2 * * * docker exec spawner sqlite3 /app/data/users.db ".backup '/app/data/backup-$(date +\%Y\%m\%d).db'"
# Backup-Rotation (7 Tage)
0 3 * * * find /path/to/backups -name "backup-*.db" -mtime +7 -delete
7. CI/CD Integration
# .github/workflows/deploy.yml
name: Deploy Spawner
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Image
run: docker build -t spawner:${{ github.sha }} .
- name: Push to Registry
run: docker push spawner:${{ github.sha }}
- name: Deploy to Production
run: |
ssh user@server "docker pull spawner:${{ github.sha }}"
ssh user@server "docker service update --image spawner:${{ github.sha }} spawner"
8. Tests
# tests/test_container_manager.py
import pytest
from container_manager import ContainerManager
@pytest.fixture
def manager():
return ContainerManager()
def test_spawn_container(manager):
container_id, port = manager.spawn_container(1, 'testuser')
assert container_id is not None
assert port == 8080
# Cleanup
manager.remove_container(container_id)
def test_container_resource_limits(manager):
container_id, _ = manager.spawn_container(2, 'testuser2')
container = manager.client.containers.get(container_id)
assert container.attrs['HostConfig']['Memory'] == 536870912 # 512 MB
assert container.attrs['HostConfig']['CpuQuota'] == 50000
FAQ
Kann ich bestehende Container wiederverwenden?
Ja! In `spawn_container()` wird geprüft ob ein Container bereits existiert:
existing = self._get_user_container(username)
if existing and existing.status == 'running':
return existing.id, self._get_container_port(existing)
Wie viele User kann das System handhaben?
Abhängig von Hardware und User-Container-Ressourcen:
- 8 GB RAM: ~10-15 User (bei 512 MB pro Container)
- 16 GB RAM: ~25-30 User
- 32 GB RAM: ~60+ User
Für >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).
Kann ich existierende User-Daten migrieren?
Ja! Volume-Mounts verwenden und bei Migration Volumes kopieren:
docker run --rm -v old-user-data:/from -v new-user-data:/to alpine sh -c "cp -av /from/* /to/"
Ressourcen
Docker SDK Documentation
Flask & Security
Traefik
Alternatives & Inspiration
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)
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
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
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
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
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
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
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
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)
FROM nginxinc/nginx-unprivileged:alpine
# Beispiel: Einfacher Webserver pro User
# HTML direkt in den Container schreiben
RUN echo '<h1>Dein persönlicher Service</h1>' > /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
<!DOCTYPE html>
<html>
<head>
<title>Login - Spawner</title>
</head>
<body>
<h2>Login</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p style="color: red;">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<input type="text" name="username" placeholder="Username" required><br>
<input type="password" name="password" placeholder="Password" required><br>
<button type="submit">Login</button>
</form>
<p>Noch kein Account? <a href="{{ url_for('auth.signup') }}">Registrieren</a></p>
</body>
</html>
templates/dashboard.html
<!DOCTYPE html>
<html>
<head>
<title>Dashboard - {{ user.username }}</title>
</head>
<body>
<h2>Willkommen, {{ user.username }}!</h2>
<p>Container-Status: <strong>{{ container_status }}</strong></p>
<p>Dein Service: <a href="{{ service_url }}" target="_blank">{{ service_url }}</a></p>
<a href="{{ url_for('restart_container') }}">Container neu starten</a><br>
<a href="{{ url_for('auth.logout') }}">Logout</a>
</body>
</html>
Starten
# 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
- Traefik-Integration über Labels brunoscheufler
- 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
# Traefik-Container finden
docker ps | grep traefik
# Traefik-Konfiguration anzeigen
docker inspect <traefik-container-name> | jq '.[0].Config.Labels'
docker inspect <traefik-container-name> | jq '.[0].HostConfig.Binds'
# Verwendetes Netzwerk ermitteln
docker inspect <traefik-container-name> | jq '.[0].NetworkSettings.Networks'
Dokumentieren:
- Traefik-Version: _______________
- Netzwerk-Name: _______________
- EntryPoints: _______________
- Zertifikats-Resolver (falls HTTPS): _______________
docker ps | grep traefik
81e0f2d0f8c0 traefik:v3.6.5
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"
}
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"
]
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
# Alle Docker-Netzwerke auflisten
docker network ls
# Netzwerk-Details des Traefik-Netzwerks
docker network inspect <traefik-network-name>
# Welche Container sind bereits angeschlossen?
docker network inspect <traefik-network-name> | jq '.[0].Containers'
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
# Ist Dashboard aktiviert?
docker exec <traefik-container> cat /etc/traefik/traefik.yml | grep -A5 "api:"
# Dashboard-URL (Standard: Port 8080)
firefox http://<traefik-host>:8080
Notiz: Dashboard-URL für Monitoring: _______________
Phase 2: Projekt-Setup
Step 2.1: Projektverzeichnis erstellen
# 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
# 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
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
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:
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:
docker exec <traefik-container> 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/:
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
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
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
docker-compose build
Step 5.4: Test-Start
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
firefox http://localhost:5000
Registriere einen Test-User und prüfe ob Container spawnt:
docker ps | grep user-
docker inspect user-testuser-1 | jq '.[0].Config.Labels'
Phase 6: Traefik-Integration aktivieren
Step 6.1: Netzwerk verbinden
# 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):
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 → <server-ip> - DNS-Propagation abwarten
Step 6.3: Traefik-Routing testen
# Traefik-Dashboard öffnen
firefox http://<traefik-host>: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
- Spawner-UI aufrufen
- Neuen User registrieren
- Zum Dashboard navigieren
- 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
docker-compose down
docker-compose up -d --build
Step 7.4: HTTPS testen
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
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
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
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
# 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
# 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
# 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
- User-Feedback sammeln
- Templates erweitern (Python, Node.js)
- Admin-Dashboard entwickeln
- Auto-Shutdown implementieren
- Volume-Persistenz aktivieren
- Multi-Region deployment
Integration abgeschlossen! 🚀
Bei Fragen:
- Logs:
docker-compose logs -f - Traefik:
http://<host>:8080 - Health:
curl https://spawner.example.com/health
