Skip to content

> 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

    mkdir -p /opt/stacks/proxy/traefik/{config,dynamic,letsencrypt}

### Step 2: Create docker-compose.yml

    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

    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)

    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

    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

    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

    #!/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:

    /opt/stacks/scripts/add-traefik-service.sh myservice myservice.nuclide.systems 8000

### Script 2: Generate Labels for Existing Services

    #!/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:

    /opt/stacks/scripts/traefik-labels-gen.sh

## Phase 5: Update Service Configs

### Update `/opt/stacks/ai/.env`

    # 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`

    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

    # 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

    # 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

    # 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

    # 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)

    # 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