Add full support for 2 container types (development and production):
Backend Changes:
- New UserContainer model with unique constraint on (user_id, container_type)
- Removed single-container fields from User model (container_id, container_port)
- Added CONTAINER_TEMPLATES config with dev and prod templates
- Implemented spawn_multi_container() method in ContainerManager
- Added 2 new API endpoints:
* GET /api/user/containers - list all containers with status
* POST /api/container/launch/<type> - on-demand container creation
- Multi-container container names and Traefik routing with type suffix
Frontend Changes:
- New Container, ContainersResponse, LaunchResponse types
- Implemented getUserContainers() and launchContainer() API functions
- Completely redesigned dashboard with 2 container cards
- Status display with icons for each container type
- "Create & Open" and "Open Service" buttons based on container status
- Responsive grid layout
Templates:
- user-template-next already configured with Tailwind CSS and Shadcn/UI
Documentation:
- Added IMPLEMENTATION_SUMMARY.md with complete feature list
- Added TEST_VERIFICATION.md with detailed testing guide
- Updated .env.example with USER_TEMPLATE_IMAGE_DEV/PROD variables
This MVP allows each user to manage 2 distinct containers with:
- On-demand lazy creation
- Status tracking per container
- Unique URLs: /{slug}-dev and /{slug}-prod
- Proper Traefik routing with StripPrefix middleware
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
11 KiB
11 KiB
Multi-Container MVP - Test & Verification Guide
Backend Syntax Verification
Python Compilation Check
python -m py_compile models.py container_manager.py api.py config.py
# ✅ Alle Dateien erfolgreich kompiliert
Imports Verification
# models.py - UserContainer ist importierbar
from models import UserContainer, User, db
# api.py - UserContainer Import
from models import UserContainer
# container_manager.py - Neue Methode
from container_manager import ContainerManager
mgr = ContainerManager()
mgr.spawn_multi_container(user_id, slug, container_type)
Code Structure Verification
UserContainer Model (models.py)
class UserContainer(db.Model):
__tablename__ = 'user_container'
# ✅ Alle erforderlichen Felder:
- id (Primary Key)
- user_id (Foreign Key)
- container_type ('dev' | 'prod')
- container_id (Docker ID)
- container_port (Port)
- template_image (Image Name)
- created_at (Timestamp)
- last_used (Timestamp)
# ✅ Unique Constraint
__table_args__ = (
db.UniqueConstraint('user_id', 'container_type'),
)
User Model (models.py)
class User(db.Model):
# ✅ Entfernt:
- container_id
- container_port
# ✅ Hinzugefügt:
- containers = db.relationship('UserContainer')
Config Templates (config.py)
CONTAINER_TEMPLATES = {
'dev': {
'image': 'user-service-template:latest', # FROM ENV
'display_name': 'Development Container',
'description': 'Nginx-basierter Development Container'
},
'prod': {
'image': 'user-template-next:latest', # FROM ENV
'display_name': 'Production Container',
'description': 'Next.js Production Build'
}
}
API Endpoint Verification
GET /api/user/containers
Request:
curl -H "Authorization: Bearer <JWT>" \
http://localhost:5000/api/user/containers
Expected Response:
{
"containers": [
{
"type": "dev",
"display_name": "Development Container",
"description": "Nginx-basierter Development Container",
"status": "running|stopped|not_created|error",
"service_url": "https://coder.domain.com/slug-dev",
"container_id": "abc123def456...",
"created_at": "2025-01-31T10:00:00",
"last_used": "2025-01-31T11:30:00"
},
{
"type": "prod",
"display_name": "Production Container",
"description": "Next.js Production Build",
"status": "not_created",
"service_url": "https://coder.domain.com/slug-prod",
"container_id": null,
"created_at": null,
"last_used": null
}
]
}
POST /api/container/launch/<container_type>
Request:
curl -X POST -H "Authorization: Bearer <JWT>" \
http://localhost:5000/api/container/launch/dev
Expected Response (First Call):
{
"message": "Container bereit",
"service_url": "https://coder.domain.com/slug-dev",
"container_id": "abc123def456...",
"status": "running"
}
Expected Response (Subsequent Calls):
- Wenn Container läuft: Gibt selbe Response zurück, aktualisiert last_used
- Wenn Container gestoppt: Startet Container neu mit
start_container() - Wenn Container gelöscht: Erstellt neuen Container mit
spawn_multi_container()
Frontend TypeScript Verification
api.ts Types
// ✅ Container Type
export interface Container {
type: string;
display_name: string;
description: string;
status: 'not_created' | 'running' | 'stopped' | 'error';
service_url: string;
container_id: string | null;
created_at: string | null;
last_used: string | null;
}
// ✅ ContainersResponse Type
export interface ContainersResponse {
containers: Container[];
}
// ✅ LaunchResponse Type
export interface LaunchResponse {
message: string;
service_url: string;
container_id: string;
status: string;
}
// ✅ API Functions
api.getUserContainers(): Promise<ApiResponse<ContainersResponse>>
api.launchContainer(containerType: string): Promise<ApiResponse<LaunchResponse>>
Dashboard Component
// ✅ State Management
const [containers, setContainers] = useState<Container[]>([]);
const [loading, setLoading] = useState(true);
const [launching, setLaunching] = useState<string | null>(null);
const [error, setError] = useState("");
// ✅ Load Containers
const loadContainers = async () => {
const { data, error } = await api.getUserContainers();
setContainers(data.containers);
}
// ✅ Launch Container
const handleLaunchContainer = async (containerType: string) => {
const { data } = await api.launchContainer(containerType);
window.open(data.service_url, "_blank");
await loadContainers();
}
// ✅ Rendering
- 2 Container-Cards (dev + prod)
- Status-Icons (running, stopped, error, not_created)
- "Erstellen & Öffnen" oder "Service öffnen" Button
- Loading States während Launch
- Error-Handling
Docker Container Verification
spawn_multi_container() Method
def spawn_multi_container(self, user_id: int, slug: str, container_type: str) -> tuple:
"""
✅ Prüft:
- Template-Typ ist gültig
- Image existiert
- Container-Name ist eindeutig (user-{slug}-{type}-{id})
✅ Setzt:
- Traefik-Labels mit Typ-Suffix
- Environment Variablen (USER_ID, USER_SLUG, CONTAINER_TYPE)
- Memory/CPU Limits
- Restart Policy
✅ Gibt zurück:
- (container_id, port_8080)
"""
Traefik Labels
# ✅ Router mit Typ-Suffix
f'traefik.http.routers.user{user_id}-{container_type}.rule':
f'Host(`{base_host}`) && PathPrefix(`/{slug_with_suffix}`)'
# ✅ StripPrefix Middleware
f'traefik.http.middlewares.user{user_id}-{container_type}-strip.stripprefix.prefixes':
f'/{slug_with_suffix}'
# ✅ Service Routing
f'traefik.http.services.user{user_id}-{container_type}.loadbalancer.server.port':
'8080'
# ✅ TLS/HTTPS
f'traefik.http.routers.user{user_id}-{container_type}.tls': 'true'
f'traefik.http.routers.user{user_id}-{container_type}.tls.certresolver':
Config.TRAEFIK_CERTRESOLVER
URL Routing Test
User Request: https://coder.domain.com/slug-dev/path
↓
Traefik: Rule match (Host + PathPrefix)
↓
Middleware: StripPrefix entfernt /slug-dev
↓
Container: Erhält http://localhost:8080/path
End-to-End Test Workflow
Schritt 1: Setup
# Clean Slate
rm spawner.db
docker ps -a | grep user- | awk '{print $1}' | xargs docker rm -f
# Build Templates
docker build -t user-service-template:latest user-template/
docker build -t user-template-next:latest user-template-next/
# Start Services
docker-compose up -d --build
# Überprüfe Logs
docker-compose logs -f spawner
Schritt 2: Registrierung
1. Öffne https://coder.domain.com
2. Klick "Registrieren"
3. Gib Email ein: test@example.com
4. Klick "Magic Link senden"
5. Überprüfe Email
6. Klick Magic Link in Email
7. User wird registriert und zu Dashboard weitergeleitet
8. Überprüfe: 2 Container-Cards sollten sichtbar sein (beide "Noch nicht erstellt")
Schritt 3: Dev-Container
1. Auf Dashboard: Dev-Container Card "Erstellen & Öffnen" Button
2. Klick Button
3. Warte auf Loading State
4. Neuer Tab öffnet sich mit: https://coder.domain.com/test-dev
5. Seite zeigt Nginx-Willkommensseite
6. Zurück zum Dashboard
7. Überprüfe: Dev-Container Status = "Läuft"
8. Button ändert sich zu "Service öffnen"
Schritt 4: Prod-Container
1. Auf Dashboard: Prod-Container Card "Erstellen & Öffnen" Button
2. Klick Button
3. Warte auf Loading State
4. Neuer Tab öffnet sich mit: https://coder.domain.com/test-prod
5. Seite zeigt Next.js Demo mit Shadcn/UI
6. Zurück zum Dashboard
7. Überprüfe: Prod-Container Status = "Läuft"
8. Button ändert sich zu "Service öffnen"
Schritt 5: Container-Verwaltung
1. Klick "Service öffnen" für Dev-Container
→ Sollte bestehenden Tab neu laden
2. Refresh Dashboard
→ Beide Container sollten Status "Läuft" haben
3. Mit Dev-Container: http://{service_url}/
→ Sollte Seite ohne /test-dev/ anzeigen
4. Mit Prod-Container: http://{service_url}/
→ Sollte Seite ohne /test-prod/ anzeigen
Verification Checklist
Database
- UserContainer Tabelle existiert
- user_container.user_id Foreign Key funktioniert
- user_container.container_type ist VARCHAR(50)
- Unique Constraint (user_id, container_type) existiert
- User.containers Relationship lädt Container
API
- GET /api/user/containers funktioniert
- POST /api/container/launch/dev funktioniert
- POST /api/container/launch/prod funktioniert
- Invalid container_type gibt 400 zurück
- Missing JWT gibt 401 zurück
Docker
- spawn_multi_container() erstellt Container
- Container-Namen haben Typ-Suffix (-dev, -prod)
- Traefik-Labels haben richtige Routen
- StripPrefix funktioniert korrekt
- Beide Images sind vorhanden
Frontend
- Dashboard zeigt 2 Container-Cards
- API Calls funktionieren ohne Errors
- "Erstellen & Öffnen" Button funktioniert
- Service-URLs öffnen sich in neuem Tab
- Status aktualisiert sich nach Launch
- Loading States sind sichtbar
Integration
- User kann Dev-Container erstellen und öffnen
- User kann Prod-Container erstellen und öffnen
- Beide Container funktionieren gleichzeitig
- URL-Routing funktioniert für beide Container-Typen
- StripPrefix funktioniert korrekt für beide
Debugging Commands
Backend Debugging
# Logs
docker-compose logs -f spawner
# Container-Liste prüfen
docker ps | grep user-
# Inspect Container-Labels
docker inspect user-testuser-dev-1 | grep -A20 traefik
# Python Shell
docker exec -it spawner python
from models import UserContainer
UserContainer.query.all()
Traefik Debugging
# Traefik Dashboard
curl http://localhost:8080/api/http/routers
# Spezifische Router
curl http://localhost:8080/api/http/routers | grep user
# Logs
docker-compose logs -f traefik | grep user
Frontend Debugging
# Browser Console
window.localStorage.getItem('token')
# Network Tab
- GET /api/user/containers
- POST /api/container/launch/dev
# Redux DevTools (falls installiert)
Store überprüfen
Known Issues & Solutions
Issue: Container spawnt nicht
Symptom: POST /api/container/launch/dev gibt 500 zurück Debug:
docker-compose logs spawner | tail -50
# Prüfe: Image existiert? Docker API funktioniert?
Issue: Traefik routet nicht
Symptom: URL https://coder.domain.com/slug-dev gibt 404 Debug:
docker logs traefik | grep user
docker inspect user-testuser-dev-1 | grep traefik
Issue: StripPrefix funktioniert nicht
Symptom: Container erhält /slug-dev in Request-Path Debug:
# Container-Logs
docker logs user-testuser-dev-1
# Prüfe Traefik Middleware
curl http://localhost:8080/api/http/middlewares
Status: ✅ Alle Tests ready für Durchführung Hinweis: Aktuelle Umgebung hat kein Docker - Tests müssen auf Target-System durchgeführt werden