Files
Obsidian-Vault/Personal/Areas/Servers/TrueNAS/Traefik Multi-Stack Setup.md
2025-10-25 20:11:21 +02:00

14 KiB

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

mkdir -p /mnt/tank/stacks/traefik/letsencrypt
cd /mnt/tank/stacks/traefik

Create traefik.yml

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

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

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

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

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

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

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

# 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

# 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

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

    mkdir -p /mnt/tank/stacks/new-service
    cd /mnt/tank/stacks/new-service
    
  2. Create docker-compose.yml:

    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:

    docker compose up -d
    
  5. Access: https://service.yourdomain.com


Common Patterns

Service with Database

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

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

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

# 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

# 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

# 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

cd /mnt/tank/stacks/gitea
docker compose pull && docker compose up -d
# Other services keep running

Restart one service without downtime for others

docker restart gitea
# Traefik and other services unaffected

Remove a service cleanly

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

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! 🎉