Files
homelab-docs/TRAEFIK.md
2026-01-11 16:17:56 -05:00

675 lines
17 KiB
Markdown

# Traefik Reverse Proxy
Documentation for Traefik reverse proxy setup, SSL certificates, and deploying new public services.
## Overview
There are **TWO separate Traefik instances** handling different services. Understanding which one to use is critical.
| Instance | Location | IP | Purpose | Managed By |
|----------|----------|-----|---------|------------|
| **Traefik-Primary** | CT 202 | **10.10.10.250** | General services | Manual config files |
| **Traefik-Saltbox** | VM 101 (Docker) | **10.10.10.100** | Saltbox services only | Saltbox Ansible |
---
## ⚠️ CRITICAL RULE: Which Traefik to Use
### When Adding ANY New Service:
**USE Traefik-Primary (CT 202 @ 10.10.10.250)** - For ALL new services
**DO NOT touch Traefik-Saltbox** - Unless you're modifying Saltbox itself
### Why This Matters:
- **Traefik-Saltbox** has complex Saltbox-managed configs (Ansible-generated)
- Messing with it breaks Plex, Sonarr, Radarr, and all media services
- Each Traefik has its own Let's Encrypt certificates
- Mixing them causes certificate conflicts and routing issues
---
## Traefik-Primary (CT 202) - For New Services
### Configuration
**Location**: Container 202 on PVE (10.10.10.250)
**Config Directory**: `/etc/traefik/`
**Main Config**: `/etc/traefik/traefik.yaml`
**Dynamic Configs**: `/etc/traefik/conf.d/*.yaml`
### Access Traefik Config
```bash
# From Mac Mini:
ssh pve 'pct exec 202 -- cat /etc/traefik/traefik.yaml'
ssh pve 'pct exec 202 -- ls /etc/traefik/conf.d/'
# Edit a service config:
ssh pve 'pct exec 202 -- vi /etc/traefik/conf.d/myservice.yaml'
# View logs:
ssh pve 'pct exec 202 -- tail -f /var/log/traefik/traefik.log'
```
### Services Using Traefik-Primary
| Service | Domain | Backend |
|---------|--------|---------|
| Excalidraw | excalidraw.htsn.io | 10.10.10.206:8080 (docker-host) |
| FindShyt | findshyt.htsn.io | 10.10.10.205 (CT 205) |
| Gitea | git.htsn.io | 10.10.10.220:3000 |
| Home Assistant | homeassistant.htsn.io | 10.10.10.110 |
| LM Dev | lmdev.htsn.io | 10.10.10.111 |
| MetaMCP | metamcp.htsn.io | 10.10.10.207:12008 (docker-host2) |
| Pi-hole | pihole.htsn.io | 10.10.10.200 |
| TrueNAS | truenas.htsn.io | 10.10.10.200 |
| Proxmox | pve.htsn.io | 10.10.10.120 |
| Copyparty | copyparty.htsn.io | 10.10.10.201 |
| AI Trade | aitrade.htsn.io | (trading server) |
| Pulse | pulse.htsn.io | 10.10.10.206:7655 (monitoring) |
| Happy | happy.htsn.io | 10.10.10.206:3002 (Happy Coder relay) |
| BlueMap | map.htsn.io | 10.10.10.207:8100 (Minecraft web map, password protected) |
---
## Traefik-Saltbox (VM 101) - DO NOT MODIFY
### Configuration
**Location**: `/opt/traefik/` inside Saltbox VM
**Managed By**: Saltbox Ansible playbooks (automatic)
**Docker Mount**: `/opt/traefik``/etc/traefik` in container
### Services Using Traefik-Saltbox
- Plex (plex.htsn.io)
- Sonarr, Radarr, Lidarr
- SABnzbd, NZBGet, qBittorrent
- Overseerr, Tautulli, Organizr
- Jackett, NZBHydra2
- Authelia (SSO authentication)
- All other Saltbox-managed containers
### View Saltbox Traefik (Read-Only)
```bash
# View config (don't edit!)
ssh pve 'qm guest exec 101 -- bash -c "docker exec traefik cat /etc/traefik/traefik.yml"'
# View logs
ssh saltbox 'docker logs -f traefik'
```
**⚠️ WARNING**: Editing Saltbox Traefik configs manually will be overwritten by Ansible and may break media services.
---
## Adding a New Public Service - Complete Workflow
Follow these steps to deploy a new service and make it accessible at `servicename.htsn.io`.
### Step 0: Deploy Your Service
First, deploy your service on the appropriate host.
#### Option A: Docker on docker-host (10.10.10.206)
```bash
ssh hutson@10.10.10.206
sudo mkdir -p /opt/myservice
cat > /opt/myservice/docker-compose.yml << 'EOF'
version: "3.8"
services:
myservice:
image: myimage:latest
ports:
- "8080:80"
restart: unless-stopped
EOF
cd /opt/myservice && sudo docker-compose up -d
```
#### Option B: New LXC Container on PVE
```bash
ssh pve 'pct create CTID local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst \
--hostname myservice --memory 2048 --cores 2 \
--net0 name=eth0,bridge=vmbr0,ip=10.10.10.XXX/24,gw=10.10.10.1 \
--rootfs local-zfs:8 --unprivileged 1 --start 1'
```
#### Option C: New VM on PVE
```bash
ssh pve 'qm create VMID --name myservice --memory 2048 --cores 2 \
--net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci'
```
### Step 1: Create Traefik Config File
Use this template for new services on **Traefik-Primary (CT 202)**:
#### Basic Template
```yaml
# /etc/traefik/conf.d/myservice.yaml
http:
routers:
# HTTPS router
myservice-secure:
entryPoints:
- websecure
rule: "Host(`myservice.htsn.io`)"
service: myservice
tls:
certResolver: cloudflare # Use 'cloudflare' for proxied domains, 'letsencrypt' for DNS-only
priority: 50
# HTTP → HTTPS redirect
myservice-redirect:
entryPoints:
- web
rule: "Host(`myservice.htsn.io`)"
middlewares:
- myservice-https-redirect
service: myservice
priority: 50
services:
myservice:
loadBalancer:
servers:
- url: "http://10.10.10.XXX:PORT"
middlewares:
myservice-https-redirect:
redirectScheme:
scheme: https
permanent: true
```
#### Deploy the Config
```bash
# Create file on CT 202
ssh pve 'pct exec 202 -- bash -c "cat > /etc/traefik/conf.d/myservice.yaml << '\''EOF'\''
<paste config here>
EOF"'
# Traefik auto-reloads (watches conf.d directory)
# Check logs:
ssh pve 'pct exec 202 -- tail -f /var/log/traefik/traefik.log'
```
### Step 2: Add Cloudflare DNS Entry
#### Cloudflare Credentials
| Field | Value |
|-------|-------|
| Email | cloudflare@htsn.io |
| API Key | 849ebefd163d2ccdec25e49b3e1b3fe2cdadc |
| Zone ID (htsn.io) | c0f5a80448c608af35d39aa820a5f3af |
| Public IP | 70.237.94.174 |
#### Method 1: Manual (Cloudflare Dashboard)
1. Go to https://dash.cloudflare.com/
2. Select `htsn.io` domain
3. DNS → Add Record
4. Type: `A`, Name: `myservice`, IPv4: `70.237.94.174`, Proxied: ☑️
#### Method 2: Automated (CLI)
Save this as `~/bin/add-cloudflare-dns.sh`:
```bash
#!/bin/bash
# Add DNS record to Cloudflare for htsn.io
SUBDOMAIN="$1"
CF_EMAIL="cloudflare@htsn.io"
CF_API_KEY="849ebefd163d2ccdec25e49b3e1b3fe2cdadc"
ZONE_ID="c0f5a80448c608af35d39aa820a5f3af"
PUBLIC_IP="70.237.94.174"
if [ -z "$SUBDOMAIN" ]; then
echo "Usage: $0 <subdomain>"
echo "Example: $0 myservice # Creates myservice.htsn.io"
exit 1
fi
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "X-Auth-Email: $CF_EMAIL" \
-H "X-Auth-Key: $CF_API_KEY" \
-H "Content-Type: application/json" \
--data "{
\"type\":\"A\",
\"name\":\"$SUBDOMAIN\",
\"content\":\"$PUBLIC_IP\",
\"ttl\":1,
\"proxied\":true
}" | jq .
```
**Usage**:
```bash
chmod +x ~/bin/add-cloudflare-dns.sh
~/bin/add-cloudflare-dns.sh myservice # Creates myservice.htsn.io
```
### Step 3: Testing
```bash
# Check if DNS resolves
dig myservice.htsn.io
# Should return: 70.237.94.174 (or Cloudflare IPs if proxied)
# Test HTTP redirect
curl -I http://myservice.htsn.io
# Expected: 301 redirect to https://
# Test HTTPS
curl -I https://myservice.htsn.io
# Expected: 200 OK
# Check Traefik dashboard (if enabled)
# http://10.10.10.250:8080/dashboard/
```
### Step 4: Update Documentation
After deploying, update:
1. **IP-ASSIGNMENTS.md** - Add to Services & Reverse Proxy Mapping table
2. **This file (TRAEFIK.md)** - Add to "Services Using Traefik-Primary" list
3. **CLAUDE.md** - Update quick reference if needed
---
## SSL Certificates
Traefik has **two certificate resolvers** configured:
| Resolver | Use When | Challenge Type | Notes |
|----------|----------|----------------|-------|
| `letsencrypt` | Cloudflare DNS-only (gray cloud ☁️) | HTTP-01 | Requires port 80 reachable |
| `cloudflare` | Cloudflare Proxied (orange cloud 🟠) | DNS-01 | Works with Cloudflare proxy |
### ⚠️ Important: HTTP Challenge vs DNS Challenge
**If Cloudflare proxy is enabled** (orange cloud), HTTP challenge **FAILS** because Cloudflare redirects HTTP→HTTPS before the challenge reaches your server.
**Solution**: Use `cloudflare` resolver (DNS-01 challenge) instead.
### Certificate Resolver Configuration
**Cloudflare API credentials** are configured in `/etc/systemd/system/traefik.service`:
```ini
Environment="CF_API_EMAIL=cloudflare@htsn.io"
Environment="CF_API_KEY=849ebefd163d2ccdec25e49b3e1b3fe2cdadc"
```
### Certificate Storage
| Resolver | Storage File |
|----------|--------------|
| HTTP challenge (`letsencrypt`) | `/etc/traefik/acme.json` |
| DNS challenge (`cloudflare`) | `/etc/traefik/acme-cf.json` |
**Permissions**: Must be `600` (read/write owner only)
```bash
# Check permissions
ssh pve 'pct exec 202 -- ls -la /etc/traefik/acme*.json'
# Fix if needed
ssh pve 'pct exec 202 -- chmod 600 /etc/traefik/acme.json'
ssh pve 'pct exec 202 -- chmod 600 /etc/traefik/acme-cf.json'
```
### Certificate Renewal
- **Automatic** via Traefik
- Checks every 24 hours
- Renews 30 days before expiry
- No manual intervention needed
### Troubleshooting Certificates
#### Certificate Fails to Issue
```bash
# Check Traefik logs
ssh pve 'pct exec 202 -- tail -f /var/log/traefik/traefik.log | grep -i error'
# Verify Cloudflare API access
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "X-Auth-Email: cloudflare@htsn.io" \
-H "X-Auth-Key: 849ebefd163d2ccdec25e49b3e1b3fe2cdadc"
# Check acme.json permissions
ssh pve 'pct exec 202 -- ls -la /etc/traefik/acme*.json'
```
#### Force Certificate Renewal
```bash
# Delete certificate (Traefik will re-request)
ssh pve 'pct exec 202 -- rm /etc/traefik/acme-cf.json'
ssh pve 'pct exec 202 -- touch /etc/traefik/acme-cf.json'
ssh pve 'pct exec 202 -- chmod 600 /etc/traefik/acme-cf.json'
ssh pve 'pct exec 202 -- systemctl restart traefik'
# Watch logs
ssh pve 'pct exec 202 -- tail -f /var/log/traefik/traefik.log'
```
---
## Quick Deployment - One-Liner
For fast deployment, use this all-in-one command:
```bash
# === DEPLOY SERVICE (example: myservice on docker-host port 8080) ===
# 1. Create Traefik config
ssh pve 'pct exec 202 -- bash -c "cat > /etc/traefik/conf.d/myservice.yaml << EOF
http:
routers:
myservice-secure:
entryPoints: [websecure]
rule: Host(\\\`myservice.htsn.io\\\`)
service: myservice
tls: {certResolver: cloudflare}
services:
myservice:
loadBalancer:
servers:
- url: http://10.10.10.206:8080
EOF"'
# 2. Add Cloudflare DNS
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/c0f5a80448c608af35d39aa820a5f3af/dns_records" \
-H "X-Auth-Email: cloudflare@htsn.io" \
-H "X-Auth-Key: 849ebefd163d2ccdec25e49b3e1b3fe2cdadc" \
-H "Content-Type: application/json" \
--data '{"type":"A","name":"myservice","content":"70.237.94.174","proxied":true}'
# 3. Test (wait a few seconds for DNS propagation)
curl -I https://myservice.htsn.io
```
---
## Docker Service with Traefik Labels (Alternative)
If deploying a service via Docker on `docker-host` (VM 206), you can use Traefik labels instead of config files.
**Requirements**:
- Traefik must have access to Docker socket
- Service must be on same Docker network as Traefik
**Example docker-compose.yml**:
```yaml
version: "3.8"
services:
myservice:
image: myimage:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.myservice.rule=Host(`myservice.htsn.io`)"
- "traefik.http.routers.myservice.entrypoints=websecure"
- "traefik.http.routers.myservice.tls.certresolver=letsencrypt"
- "traefik.http.services.myservice.loadbalancer.server.port=8080"
networks:
- traefik
networks:
traefik:
external: true
```
**Note**: This method is NOT currently used on Traefik-Primary (CT 202), as it doesn't have Docker socket access. Config files are preferred.
---
## Cloudflare API Reference
### API Credentials
| Field | Value |
|-------|-------|
| Email | cloudflare@htsn.io |
| API Key | 849ebefd163d2ccdec25e49b3e1b3fe2cdadc |
| Zone ID | c0f5a80448c608af35d39aa820a5f3af |
### Common API Operations
Set credentials:
```bash
CF_EMAIL="cloudflare@htsn.io"
CF_API_KEY="849ebefd163d2ccdec25e49b3e1b3fe2cdadc"
ZONE_ID="c0f5a80448c608af35d39aa820a5f3af"
```
**List all DNS records**:
```bash
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "X-Auth-Email: $CF_EMAIL" \
-H "X-Auth-Key: $CF_API_KEY" | jq
```
**Add A record**:
```bash
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "X-Auth-Email: $CF_EMAIL" \
-H "X-Auth-Key: $CF_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"type":"A",
"name":"subdomain",
"content":"70.237.94.174",
"proxied":true
}'
```
**Delete record**:
```bash
curl -X DELETE "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "X-Auth-Email: $CF_EMAIL" \
-H "X-Auth-Key: $CF_API_KEY"
```
**Update record** (toggle proxy):
```bash
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "X-Auth-Email: $CF_EMAIL" \
-H "X-Auth-Key: $CF_API_KEY" \
-H "Content-Type: application/json" \
--data '{"proxied":false}'
```
---
## Troubleshooting
### Service Not Accessible
```bash
# 1. Check if DNS resolves
dig myservice.htsn.io
# 2. Check if backend is reachable
curl -I http://10.10.10.XXX:PORT
# 3. Check Traefik logs
ssh pve 'pct exec 202 -- tail -f /var/log/traefik/traefik.log'
# 4. Check Traefik config is valid
ssh pve 'pct exec 202 -- cat /etc/traefik/conf.d/myservice.yaml'
# 5. Restart Traefik (if needed)
ssh pve 'pct exec 202 -- systemctl restart traefik'
```
### Certificate Issues
```bash
# Check certificate status in acme.json
ssh pve 'pct exec 202 -- cat /etc/traefik/acme-cf.json | jq'
# Check certificate expiry
echo | openssl s_client -servername myservice.htsn.io -connect myservice.htsn.io:443 2>/dev/null | openssl x509 -noout -dates
```
### 502 Bad Gateway
**Cause**: Backend service is down or unreachable
```bash
# Check if backend is running
ssh backend-host 'systemctl status myservice'
# Check if port is open
nc -zv 10.10.10.XXX PORT
# Check firewall
ssh backend-host 'iptables -L -n | grep PORT'
```
### 404 Not Found
**Cause**: Traefik can't match the request to a router
```bash
# Check router rule matches domain
ssh pve 'pct exec 202 -- cat /etc/traefik/conf.d/myservice.yaml | grep rule'
# Should be: rule: "Host(`myservice.htsn.io`)"
# Check DNS is pointing to correct IP
dig myservice.htsn.io
# Restart Traefik to reload config
ssh pve 'pct exec 202 -- systemctl restart traefik'
```
---
## Advanced Configuration Examples
### WebSocket Support
For services that use WebSockets (like Home Assistant):
```yaml
http:
routers:
myservice-secure:
entryPoints:
- websecure
rule: "Host(`myservice.htsn.io`)"
service: myservice
tls:
certResolver: cloudflare
services:
myservice:
loadBalancer:
servers:
- url: "http://10.10.10.XXX:PORT"
# No special config needed - WebSockets work by default in Traefik v2+
```
### Custom Headers
Add custom headers (e.g., security headers):
```yaml
http:
routers:
myservice-secure:
middlewares:
- myservice-headers
middlewares:
myservice-headers:
headers:
customResponseHeaders:
X-Frame-Options: "DENY"
X-Content-Type-Options: "nosniff"
Referrer-Policy: "strict-origin-when-cross-origin"
```
### Basic Authentication
Protect a service with basic auth:
```yaml
http:
routers:
myservice-secure:
middlewares:
- myservice-auth
middlewares:
myservice-auth:
basicAuth:
users:
- "user:$apr1$..." # Generate with: htpasswd -nb user password
```
---
## Maintenance
### Monthly Checks
```bash
# Check Traefik status
ssh pve 'pct exec 202 -- systemctl status traefik'
# Review logs for errors
ssh pve 'pct exec 202 -- grep -i error /var/log/traefik/traefik.log | tail -20'
# Check certificate expiry dates
ssh pve 'pct exec 202 -- cat /etc/traefik/acme-cf.json | jq ".cloudflare.Certificates[] | {domain: .domain.main, expiry: .certificate}"'
# Verify all services responding
for domain in plex.htsn.io git.htsn.io truenas.htsn.io; do
echo "Testing $domain..."
curl -sI https://$domain | head -1
done
```
### Backup Traefik Config
```bash
# Backup all configs
ssh pve 'pct exec 202 -- tar czf /tmp/traefik-backup-$(date +%Y%m%d).tar.gz /etc/traefik'
# Copy to safe location
scp "pve:/var/lib/lxc/202/rootfs/tmp/traefik-backup-*.tar.gz" ~/Backups/traefik/
```
---
## Related Documentation
- [IP-ASSIGNMENTS.md](IP-ASSIGNMENTS.md) - Service IP addresses
- [CLOUDFLARE.md](#) - Cloudflare DNS management (coming soon)
- [SERVICES.md](#) - Complete service inventory (coming soon)
---
**Last Updated**: 2025-12-22