581 lines
14 KiB
Markdown
581 lines
14 KiB
Markdown
# 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! 🎉
|