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)
- Cloudflare Dashboard → Profile → API Tokens
- View Global API Key
- Use with
CF_API_EMAILandCF_API_KEY
Option B: API Token (recommended)
- Cloudflare Dashboard → Profile → API Tokens → Create Token
- Use template: "Edit zone DNS"
- Permissions: Zone → DNS → Edit
- Zone Resources: Include → Specific zone → yourdomain.com
- 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_proxynetwork - 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)
-
Create service directory:
mkdir -p /mnt/tank/stacks/new-service cd /mnt/tank/stacks/new-service -
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" -
Add DNS record in Cloudflare:
service.yourdomain.com → Your-IP -
Deploy:
docker compose up -d -
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! 🎉