> **STATUS: ABANDONED 2026-05-16 — Zoraxy is the production reverse proxy. Kept for design-decision history.** --- # Traefik Migration Guide Using Docker Labels ## Overview Migrate from Zoraxy reverse proxy to Traefik using Docker labels for zero-touch service discovery. --- ## Phase 1: Install Traefik ### Step 1: Create Directory Structure ```bash mkdir -p /opt/stacks/proxy/traefik/{config,dynamic,letsencrypt} ``` ### Step 2: Create docker-compose.yml ```yaml version: "3.8" services: traefik: image: traefik:v3.2 container_name: traefik restart: always network_mode: host security_opt: - no-new-privileges=true ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /opt/stacks/proxy/traefik/config:/etc/traefik - /opt/stacks/proxy/traefik/dynamic:/etc/traefik/dynamic - /opt/stacks/proxy/traefik/letsencrypt:/etc/letsencrypt command: - "--api.insecure=true" - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--providers.docker.network=ai-internal" - "--providers.docker.network=shared_backend" - "--providers.docker.defaultRule=Host(`{{ .Name }}.nuclide.systems`)" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.httpChallenge=true" - "--certificatesresolvers.letsencrypt.acme.email=admin@nuclide.systems" - "--certificatesresolvers.letsencrypt.acme.storage=/etc/letsencrypt/acme.json" ``` ### Step 3: Start Traefik ```bash cd /opt/stacks/proxy/traefik docker compose up -d # Verify docker compose ps ``` --- ## Phase 2: Migrate AI Services ### AI Service Labels (Add to litellm, chat, mcp-compose.yml) ```yaml services: litellm: image: litellm labels: - "traefik.enable=true" - "traefik.http.routers.litellm.rule=Host(`litellm.nuclide.systems`)" - "traefik.http.routers.litellm.entrypoints=websecure" - "traefik.http.routers.litellm.tls=true" - "traefik.http.routers.litellm.tls.certresolver=letsencrypt" - "traefik.http.routers.litellm.priority=10" - "traefik.http.services.litellm.loadbalancer.server.port=14000" chat: image: lobehub labels: - "traefik.enable=true" - "traefik.http.routers.chat.rule=Host(`chat.nuclide.systems`)" - "traefik.http.routers.chat.entrypoints=websecure" - "traefik.http.routers.chat.tls=true" - "traefik.http.routers.chat.tls.certresolver=letsencrypt" - "traefik.http.routers.chat.priority=10" - "traefik.http.services.chat.loadbalancer.server.port=14001" mcp: image: mcp-gateway labels: - "traefik.enable=true" - "traefik.http.routers.mcp.rule=Host(`mcp.nuclide.systems`)" - "traefik.http.routers.mcp.entrypoints=websecure" - "traefik.http.routers.mcp.tls=true" - "traefik.http.routers.mcp.tls.certresolver=letsencrypt" - "traefik.http.routers.mcp.priority=10" - "traefik.http.services.mcp.loadbalancer.server.port=8080" ``` --- ## Phase 3: Migrate Garage S3 ### Option A: Use Traefik Proxy ```yaml services: garage-proxy: image: nginx:alpine labels: - "traefik.enable=true" - "traefik.http.routers.s3.rule=Host(`s3.nuclide.systems`)" - "traefik.http.routers.s3.entrypoints=websecure" - "traefik.http.routers.s3.tls=true" - "traefik.http.routers.s3.tls.certresolver=letsencrypt" - "traefik.http.services.s3.loadbalancer.server.port=10004" volumes: - garage-data:/data networks: - shared_backend ``` ### Option B: Keep Internal Garage Access ```yaml services: # No proxy needed - access Garage via internal IP:10004 garage: image: garageio/garage ports: - "3900:3900" # Internal only - "10004:10004" # Public via Traefik ``` --- ## Phase 4: Create Helper Scripts ### script 1: Add Service to Traefik ```bash #!/bin/bash # /opt/stacks/scripts/add-traefik-service.sh NAME=$1 DOMAIN=$2 PORT=$3 cat > /opt/stacks/proxy/traefik/dynamic/${NAME}.yml << EOF http: routers: ${NAME}-router: rule: "Host(\`${DOMAIN}\`)" service: ${NAME}-service entrypoints: - websecure tls: certresolver: letsencrypt services: ${NAME}-service: loadBalancer: servers: - url: "http://192.168.1.40:${PORT}" EOF # Reload Traefik docker automatically (no manual step needed) echo "✅ Service ${NAME} added via labels" ``` Usage: ```bash /opt/stacks/scripts/add-traefik-service.sh myservice myservice.nuclide.systems 8000 ``` ### Script 2: Generate Labels for Existing Services ```bash #!/bin/bash # /opt/stacks/scripts/traefik-labels-gen.sh cat > /opt/stacks/proxy/traefik/labels.yaml << 'EOF' # Add these labels to service docker-compose.yml files # LiteLLM services: litellm: labels: - "traefik.enable=true" - "traefik.http.routers.litellm.rule=Host(`litellm.nuclide.systems`)" - "traefik.http.routers.litellm.entrypoints=websecure" - "traefik.http.routers.litellm.tls=true" - "traefik.http.routers.litellm.tls.certresolver=letsencrypt" - "traefik.http.services.litellm.loadbalancer.server.port=14000" # LobeHub Chat services: chat: labels: - "traefik.enable=true" - "traefik.http.routers.chat.rule=Host(`chat.nuclide.systems`)" - "traefik.http.routers.chat.entrypoints=websecure" - "traefik.http.routers.chat.tls=true" - "traefik.http.routers.chat.tls.certresolver=letsencrypt" - "traefik.http.services.chat.loadbalancer.server.port=14001" # MCP Gateway services: mcp-gateway: labels: - "traefik.enable=true" - "traefik.http.routers.mcp.rule=Host(`mcp.nuclide.systems`)" - "traefik.http.routers.mcp.entrypoints=websecure" - "traefik.http.routers.mcp.tls=true" - "traefik.http.routers.mcp.tls.certresolver=letsencrypt" - "traefik.http.services.mcp.loadbalancer.server.port=8080" EOF echo "✅ Labels saved to /opt/stacks/proxy/traefik/labels.yaml" ``` Usage: ```bash /opt/stacks/scripts/traefik-labels-gen.sh ``` --- ## Phase 5: Update Service Configs ### Update `/opt/stacks/ai/.env` ```bash # OLD (Zoraxy): LITELLM_BASE_URL=https://litellm.nuclide.systems PROXY_BASE_URL=https://mcp.nuclide.systems # NEW (Traefik) - same URLs, different backend: LITELLM_BASE_URL=https://litellm.nuclide.systems PROXY_BASE_URL=https://mcp.nuclide.systems CHATAI_BASE_URL=https://chat.nuclide.systems ``` ### Update `/opt/stacks/ai/litellm-config/config.yaml` ```yaml general_settings: proxy_base_url: https://litellm.nuclide.systems control_plane_url: https://litellm.nuclide.systems ``` --- ## Phase 6: Verify SSL ### Step 1: Generate Let's Encrypt Certificates ```bash # Verify Traefik is running docker compose ps traefik # Create ACME cert file touch /opt/stacks/proxy/traefik/letsencrypt/acme.json chmod 600 /opt/stacks/proxy/traefik/letsencrypt/acme.json # Trigger certificate generation (will happen automatically) # Check status: curl -s https://acme-v02.api.letsencrypt.org/directory | head ``` ### Step 2: Test HTTPS ```bash # Test endpoints curl -k https://litellm.nuclide.systems/health curl -k https://chat.nuclide.systems/health curl -k https://mcp.nuclide.systems/health # Verify cert curl -v https://litellm.nuclide.systems 2>&1 | grep -A 5 "SSL certificate" ``` --- ## Phase 7: Remove Zoraxy ### Backup first ```bash # Backup Zoraxy configs docker cp zoraxy:/data/configs /backup/zoraxy-backup/ # Optional: Stop Zoraxy docker stop zoraxy docker rm zoraxy ``` --- ## Quick Migration Checklist - [ ] Install Traefik (Phase 1) - [ ] Add labels to AI services (Phase 2) - [ ] Add Garage S3 proxy (Phase 3) - [ ] Run label generation script (Phase 4) - [ ] Update `.env` and config files (Phase 5) - [ ] Wait for SSL certificates (auto 24-48h) - [ ] Test all HTTPS endpoints - [ ] Remove Zoraxy (Phase 7) - [ ] Update AdGuard DNS if needed (optional) --- ## Monitoring & Troubleshooting ### Check Traefik Dashboard ```bash # Access web UI (unsecured - use only on trusted network) open http://192.168.1.40:8080/dashboard # View all routers curl http://localhost:8080/api/http/routers | jq '.[] | {name: .rule, status: .entryPoints}' # View all services curl http://localhost:8080/api/http/services | jq '.[] | {name: .name, servers: .servers}' ``` ### Common Issues | Issue | Solution | |-------|----------| | 404 errors | Check router labels match domain exactly | | SSL expired | Wait for auto-renew or trigger manually | | Port mismatch | Verify `loadbalancer.server.port` matches service | | No SSL cert | Check email in `acme.json` config | --- ## Rollback Plan (If Needed) ```bash # Stop Traefik docker compose -f /opt/stacks/proxy/traefik/docker-compose.yml down # Restore Zoraxy configs docker cp /backup/zoraxy-backup/ configs/ # Restart Zoraxy (if you kept backup) docker start zoraxy || true ```