346 lines
8.8 KiB
Markdown
346 lines
8.8 KiB
Markdown
> **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
|
|
```
|