Init
This commit is contained in:
580
Personal/Areas/Servers/TrueNAS/Traefik Multi-Stack Setup.md
Normal file
580
Personal/Areas/Servers/TrueNAS/Traefik Multi-Stack Setup.md
Normal file
@@ -0,0 +1,580 @@
|
||||
# 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! 🎉
|
||||
Reference in New Issue
Block a user