# Traefik Multi-Stack Setup ## Architecture Overview **One Traefik instance** serves as the reverse proxy for **multiple independent Docker Compose stacks**. ``` ┌─────────────────────────────────────────┐ │ Traefik (Port 80/443) │ │ (Handles SSL & Routing) │ └─────────────────┬───────────────────────┘ │ traefik_proxy network ┌─────────┼─────────┬──────────────┐ │ │ │ │ ┌───▼───┐ ┌──▼────┐ ┌──▼─────┐ ┌───▼────┐ │ Gitea │ │ Other │ │ Future │ │ Future │ │ Stack │ │ Stack │ │ Stack │ │ Stack │ └───────┘ └───────┘ └────────┘ └────────┘ ``` **Benefits:** - Each service stack is independent (update/restart without affecting others) - One SSL certificate manager for all services - Easy to add new services - Clean separation of concerns - Each service has its own docker-compose.yml --- ## Directory Structure ``` /mnt/tank/stacks/ ├── traefik/ # Traefik stack │ ├── docker-compose.yml │ ├── traefik.yml │ └── letsencrypt/ │ └── acme.json ├── gitea/ # Gitea stack │ ├── docker-compose.yml │ └── data/ ├── servarr/ # Your existing Servarr stack │ ├── docker-compose.yml │ └── data/ └── future-service/ # Future additions └── docker-compose.yml ``` --- ## Step 1: Deploy Traefik (Once) This is the **only** Traefik instance you need. ### Create Directory ```bash mkdir -p /mnt/tank/stacks/traefik/letsencrypt cd /mnt/tank/stacks/traefik ``` ### Create `traefik.yml` ```yaml api: dashboard: true insecure: false entryPoints: web: address: ":80" http: redirections: entryPoint: to: websecure scheme: https websecure: address: ":443" providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false # Only expose containers with traefik.enable=true network: traefik_proxy # Default network to use certificatesResolvers: cloudflare: acme: email: your-email@example.com storage: /letsencrypt/acme.json # Use DNS challenge for Cloudflare (better for wildcard certs) dnsChallenge: provider: cloudflare resolvers: - "1.1.1.1:53" - "1.0.0.1:53" ``` ### Create `docker-compose.yml` ```yaml version: '3.8' networks: traefik_proxy: name: traefik_proxy driver: bridge services: traefik: image: traefik:v2.10 container_name: traefik restart: unless-stopped security_opt: - no-new-privileges:true networks: - traefik_proxy ports: - "80:80" - "443:443" environment: - TZ=America/New_York # Cloudflare API credentials for DNS challenge - CF_API_EMAIL=your-cloudflare-email@example.com - CF_API_KEY=your-cloudflare-global-api-key # Or use API Token (recommended): # - CF_DNS_API_TOKEN=your-cloudflare-api-token volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik.yml:/traefik.yml:ro - ./letsencrypt:/letsencrypt labels: - "traefik.enable=true" # Dashboard - "traefik.http.routers.traefik.rule=Host(`traefik.yourdomain.com`)" - "traefik.http.routers.traefik.entrypoints=websecure" - "traefik.http.routers.traefik.tls.certresolver=cloudflare" - "traefik.http.routers.traefik.service=api@internal" # Basic auth for dashboard # Generate: echo $(htpasswd -nb admin yourpassword) | sed -e s/\\$/\\$\\$/g - "traefik.http.routers.traefik.middlewares=traefik-auth" - "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/" ``` ### Get Cloudflare API Key **Option A: Global API Key (easier but less secure)** 1. Cloudflare Dashboard → Profile → API Tokens 2. View Global API Key 3. Use with `CF_API_EMAIL` and `CF_API_KEY` **Option B: API Token (recommended)** 1. Cloudflare Dashboard → Profile → API Tokens → Create Token 2. Use template: "Edit zone DNS" 3. Permissions: Zone → DNS → Edit 4. Zone Resources: Include → Specific zone → yourdomain.com 5. Use with `CF_DNS_API_TOKEN` ### Deploy Traefik ```bash cd /mnt/tank/stacks/traefik docker compose up -d # Check logs docker logs traefik -f ``` --- ## Step 2: Connect Gitea to Traefik Gitea runs in its **own separate stack**, but connects to Traefik's network. ### Create `docker-compose.yml` for Gitea ```bash mkdir -p /mnt/tank/stacks/gitea cd /mnt/tank/stacks/gitea ``` **Key changes from standalone:** - Connects to **external** `traefik_proxy` network - Uses Traefik labels for routing - No exposed ports except SSH (Traefik handles HTTPS) ```yaml version: '3.8' networks: traefik_proxy: external: true # Uses Traefik's network gitea_internal: driver: bridge # Internal network for Gitea (if needed for DB) services: gitea: image: gitea/gitea:latest container_name: gitea restart: unless-stopped environment: - USER_UID=1000 - USER_GID=1000 - GITEA__database__DB_TYPE=sqlite3 - GITEA__server__DOMAIN=git.yourdomain.com - GITEA__server__SSH_DOMAIN=git.yourdomain.com - GITEA__server__ROOT_URL=https://git.yourdomain.com - GITEA__server__SSH_PORT=2222 - GITEA__server__SSH_LISTEN_PORT=22 networks: - traefik_proxy # Connect to Traefik - gitea_internal # Internal network volumes: - ./data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports: - "2222:22" # Only SSH needs to be exposed directly labels: - "traefik.enable=true" - "traefik.docker.network=traefik_proxy" # HTTP/HTTPS routing - "traefik.http.routers.gitea.rule=Host(`git.yourdomain.com`)" - "traefik.http.routers.gitea.entrypoints=websecure" - "traefik.http.routers.gitea.tls.certresolver=cloudflare" - "traefik.http.services.gitea.loadbalancer.server.port=3000" ``` ### Deploy Gitea ```bash cd /mnt/tank/stacks/gitea docker compose up -d # Check logs docker logs gitea -f ``` --- ## Step 3: Connect Your Other Stack to Traefik For any other service, follow the same pattern: ### Example: Generic Service ```yaml version: '3.8' networks: traefik_proxy: external: true # Connect to Traefik's network services: your-service: image: your-service-image:latest container_name: your-service restart: unless-stopped networks: - traefik_proxy volumes: - ./data:/data # NO ports exposed - Traefik handles routing labels: - "traefik.enable=true" - "traefik.docker.network=traefik_proxy" - "traefik.http.routers.your-service.rule=Host(`service.yourdomain.com`)" - "traefik.http.routers.your-service.entrypoints=websecure" - "traefik.http.routers.your-service.tls.certresolver=cloudflare" - "traefik.http.services.your-service.loadbalancer.server.port=8080" # Internal port ``` ### Deploy Your Service ```bash cd /mnt/tank/stacks/your-service-name docker compose up -d ``` **That's it!** Traefik automatically: - Detects the new container - Creates routes based on labels - Generates SSL certificate - Starts routing traffic --- ## Cloudflare DNS Configuration For each service, add an A record in Cloudflare: ### DNS Records | Type | Name | Content | Proxy Status | |------|------|---------|--------------| | A | git | Your-Public-IP | DNS Only (gray) | | A | traefik | Your-Public-IP | DNS Only (gray) | | A | service | Your-Public-IP | DNS Only (gray) | **Important:** Set to "DNS only" (gray cloud), not proxied (orange cloud), unless you want Cloudflare's proxy in front (requires additional Traefik config). --- ## Port Forwarding Only forward **once** for Traefik: | External Port | Internal Port | Protocol | Service | |--------------|---------------|----------|---------| | 80 | 80 | TCP | HTTP (Traefik) | | 443 | 443 | TCP | HTTPS (Traefik) | | 2222 | 2222 | TCP | SSH (Gitea only) | **Additional service-specific ports** (like Gitea SSH on 2222) are forwarded individually. --- ## Managing Multiple Stacks ### Start/Stop Individual Stacks ```bash # Stop Gitea only (doesn't affect Traefik or other services) cd /mnt/tank/stacks/gitea docker compose down # Start Gitea docker compose up -d # Restart Gitea docker compose restart ``` ### Update Individual Stacks ```bash # Update just Gitea cd /mnt/tank/stacks/gitea docker compose pull docker compose up -d # Update just Traefik cd /mnt/tank/stacks/traefik docker compose pull docker compose up -d ``` ### View All Services ```bash # See all containers connected to Traefik docker network inspect traefik_proxy # See all running containers docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" ``` --- ## Adding New Services (Quick Template) 1. **Create service directory:** ```bash mkdir -p /mnt/tank/stacks/new-service cd /mnt/tank/stacks/new-service ``` 2. **Create `docker-compose.yml`:** ```yaml version: '3.8' networks: traefik_proxy: external: true services: service-name: image: service-image:latest container_name: service-name restart: unless-stopped networks: - traefik_proxy volumes: - ./data:/data labels: - "traefik.enable=true" - "traefik.docker.network=traefik_proxy" - "traefik.http.routers.service-name.rule=Host(`service.yourdomain.com`)" - "traefik.http.routers.service-name.entrypoints=websecure" - "traefik.http.routers.service-name.tls.certresolver=cloudflare" - "traefik.http.services.service-name.loadbalancer.server.port=INTERNAL_PORT" ``` 3. **Add DNS record** in Cloudflare: `service.yourdomain.com → Your-IP` 4. **Deploy:** ```bash docker compose up -d ``` 5. **Access:** `https://service.yourdomain.com` --- ## Common Patterns ### Service with Database ```yaml version: '3.8' networks: traefik_proxy: external: true internal: driver: bridge services: app: image: app-image:latest networks: - traefik_proxy # For external access - internal # For database access labels: - "traefik.enable=true" - "traefik.docker.network=traefik_proxy" - "traefik.http.routers.app.rule=Host(`app.yourdomain.com`)" - "traefik.http.routers.app.entrypoints=websecure" - "traefik.http.routers.app.tls.certresolver=cloudflare" - "traefik.http.services.app.loadbalancer.server.port=3000" db: image: postgres:15 networks: - internal # Only on internal network (not exposed) volumes: - ./db-data:/var/lib/postgresql/data ``` ### Multiple Domains for One Service ```yaml labels: - "traefik.enable=true" - "traefik.http.routers.app.rule=Host(`app.yourdomain.com`) || Host(`app2.yourdomain.com`)" - "traefik.http.routers.app.entrypoints=websecure" - "traefik.http.routers.app.tls.certresolver=cloudflare" ``` ### Service with Path Prefix ```yaml labels: - "traefik.enable=true" - "traefik.http.routers.app.rule=Host(`yourdomain.com`) && PathPrefix(`/app`)" - "traefik.http.routers.app.entrypoints=websecure" - "traefik.http.routers.app.tls.certresolver=cloudflare" - "traefik.http.middlewares.app-stripprefix.stripprefix.prefixes=/app" - "traefik.http.routers.app.middlewares=app-stripprefix" ``` --- ## Troubleshooting Multi-Stack Setup ### Service Not Accessible ```bash # Check if container is on traefik_proxy network docker network inspect traefik_proxy # Check Traefik logs for routing issues docker logs traefik | grep -i error # Verify DNS resolves correctly nslookup service.yourdomain.com # Check if Traefik sees the service docker exec traefik wget -O- http://localhost:8080/api/http/routers | grep service-name ``` ### SSL Certificate Issues ```bash # Check Traefik logs for ACME errors docker logs traefik | grep -i acme # Verify Cloudflare credentials are correct docker exec traefik env | grep CF_ # Delete acme.json to force renewal (last resort) docker compose down rm letsencrypt/acme.json docker compose up -d ``` ### Network Conflicts ```bash # List all networks docker network ls # Inspect traefik_proxy network docker network inspect traefik_proxy # Recreate network if needed docker network rm traefik_proxy cd /mnt/[pool]/docker/traefik docker compose up -d ``` --- ## Stack Independence Benefits ✅ **Update one service without affecting others** ```bash cd /mnt/tank/stacks/gitea docker compose pull && docker compose up -d # Other services keep running ``` ✅ **Restart one service without downtime for others** ```bash docker restart gitea # Traefik and other services unaffected ``` ✅ **Remove a service cleanly** ```bash cd /mnt/tank/stacks/old-service docker compose down rm -rf /mnt/tank/stacks/old-service # Done! No leftover configs in other stacks ``` ✅ **Easy backups per service** ```bash tar -czf gitea-backup.tar.gz /mnt/tank/stacks/gitea/ tar -czf servarr-backup.tar.gz /mnt/tank/stacks/servarr/ ``` --- ## Next Steps - [ ] Deploy Traefik (one time) - [ ] Get Cloudflare API credentials - [ ] Configure DNS records for services - [ ] Deploy Gitea stack - [ ] Connect your existing stack to Traefik - [ ] Test accessing all services via HTTPS - [ ] Set up automated backups per stack --- ## Example Full Setup ``` /mnt/tank/stacks/ ├── traefik/ │ ├── docker-compose.yml # Traefik │ ├── traefik.yml │ └── letsencrypt/ ├── gitea/ │ ├── docker-compose.yml # Gitea stack │ └── data/ ├── servarr/ │ ├── docker-compose.yml # Servarr stack (your existing) │ └── ... ├── nextcloud/ │ ├── docker-compose.yml # Nextcloud stack │ ├── app-data/ │ └── db-data/ └── vaultwarden/ ├── docker-compose.yml # Vaultwarden stack └── data/ ``` Each stack is independent, but all route through Traefik! 🎉