Traefik Reverse Proxy - Project Reference Pattern¶
Component: Reverse Proxy & Subdomain Routing Status: 🟡 In Development Created: 2025-12-30 Last Updated: 2025-12-30
Overview¶
Purpose¶
This PRP provides implementation patterns for Traefik reverse proxy configuration in the Chaverim ALPR Platform. Traefik handles subdomain-based routing, SSL/TLS termination, and service discovery for all platform components.
Traefik is configured via Docker labels on services, enabling automatic route discovery without configuration file management. This approach follows the "infrastructure as code" principle where routing is defined alongside services.
Scope¶
Responsibilities:
- Subdomain routing (app., ingest., docs., traefik.)
- SSL/TLS certificate management via Let's Encrypt
- Request routing and load balancing
- Middleware configuration (rate limiting, headers, authentication)
- Service health checks
Out of Scope: - Application-level authentication (handled by FastAPI) - API key validation (handled by ingest service) - Application business logic
Dependencies¶
Requires:
- Docker and Docker Compose
- DNS records for *.chvrm.flowcmd.io
- Port 80 and 443 open on host
Used By:
- Central web app (app.chvrm.flowcmd.io)
- Ingest service (ingest.chvrm.flowcmd.io)
- Documentation site (docs.chvrm.flowcmd.io)
Quick Reference¶
When to Use These Patterns¶
✅ Use when: - Adding a new service that needs external access - Configuring SSL certificates - Adding middleware (rate limiting, security headers) - Setting up health checks for services
❌ Don't use when: - Internal service-to-service communication (use Docker network) - Database access (never expose via Traefik) - Services that should only be internal
Key Patterns¶
- Docker Labels for Routing: Define routes via service labels
- Entrypoint Separation: HTTP (80) redirects to HTTPS (443)
- Let's Encrypt ACME: Automatic certificate management
- Middleware Chains: Compose security and rate limiting
- Service Discovery: Traefik auto-discovers Docker services
Domain Structure¶
| Subdomain | Service | Internal Port |
|---|---|---|
app.chvrm.flowcmd.io |
central-app |
8000 |
ingest.chvrm.flowcmd.io |
ingest |
8001 |
docs.chvrm.flowcmd.io |
docs |
8080 |
traefik.chvrm.flowcmd.io |
traefik |
8080 (dashboard) |
Implementation Task List¶
Use this checklist when implementing Traefik:
Setup & Configuration¶
- [ ] Add Traefik service to
docker-compose.yml - [ ] Create
traefik/directory for static configuration - [ ] Configure Let's Encrypt ACME
- [ ] Set up certificate storage volume
- [ ] Create Docker network for Traefik
DNS Configuration¶
- [ ] Create wildcard A record for
*.chvrm.flowcmd.io - [ ] Verify DNS propagation
- [ ] Test certificate issuance
Service Labels¶
- [ ] Add routing labels to
central-appservice - [ ] Add routing labels to
ingestservice - [ ] Add routing labels to
docsservice - [ ] Configure Traefik dashboard (internal access only)
Security¶
- [ ] Configure HTTPS redirect middleware
- [ ] Add security headers middleware
- [ ] Set up rate limiting for app endpoints
- [ ] Configure basic auth for dashboard
- [ ] Verify TLS 1.2+ enforcement
Testing¶
- [ ] Test HTTP to HTTPS redirect
- [ ] Test subdomain routing
- [ ] Test certificate renewal (staging first)
- [ ] Test middleware chains
- [ ] Verify health checks work
Patterns¶
Pattern: Docker Labels for Service Routing¶
Problem: Need to route traffic to services without managing separate configuration files.
Solution: Use Docker labels to declare routing rules. Traefik discovers services automatically.
Implementation:
# docker-compose.yml
services:
central-app:
image: alpr-central:latest
labels:
# Enable Traefik for this service
- "traefik.enable=true"
# Router: Match requests for app subdomain
- "traefik.http.routers.app.rule=Host(`app.chvrm.flowcmd.io`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
# Service: Tell Traefik which port to use
- "traefik.http.services.app.loadbalancer.server.port=8000"
# Middleware: Apply security headers
- "traefik.http.routers.app.middlewares=security-headers@file"
networks:
- traefik-public
When to Use: - Every service that needs external access - Services with custom routing requirements
Trade-offs: - Pros: No separate config files, routing lives with service - Cons: Labels can get verbose for complex routing
Pattern: Let's Encrypt Certificate Configuration¶
Problem: Need automatic SSL certificate provisioning and renewal.
Solution: Configure ACME with Let's Encrypt using TLS-ALPN-01 or DNS-01 challenge.
Implementation:
# traefik/traefik.yml (static configuration)
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
certificatesResolvers:
letsencrypt:
acme:
email: admin@chaverim.org
storage: /letsencrypt/acme.json
tlsChallenge: {} # TLS-ALPN-01 challenge
For Wildcard Certificates (DNS-01):
certificatesResolvers:
letsencrypt:
acme:
email: admin@chaverim.org
storage: /letsencrypt/acme.json
dnsChallenge:
provider: cloudflare # Or your DNS provider
delayBeforeCheck: 30
Environment Variables for DNS Challenge:
When to Use: - TLS-ALPN-01: Simple setups, individual certificates - DNS-01: Wildcard certificates, complex setups
Trade-offs: - TLS-ALPN-01 Pros: Simple, no DNS provider integration - TLS-ALPN-01 Cons: Requires port 443 open during challenge - DNS-01 Pros: Wildcard support, works behind firewalls - DNS-01 Cons: Requires DNS provider API access
Pattern: Middleware Chain for Security Headers¶
Problem: Need consistent security headers across all services.
Solution: Define middleware in static config, reference in service labels.
Implementation:
# traefik/dynamic/middlewares.yml
http:
middlewares:
security-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
frameDeny: true
contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
rate-limit-auth:
rateLimit:
average: 10
burst: 20
period: 1m
Apply to Service:
When to Use: - All public-facing services - Authentication endpoints (with rate limiting)
Pattern: Ingest Service Configuration¶
Problem: Ingest service needs high throughput, large request bodies, no rate limiting.
Solution: Configure with appropriate limits and skip rate limiting middleware.
Implementation:
services:
ingest:
image: alpr-ingest:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.ingest.rule=Host(`ingest.chvrm.flowcmd.io`)"
- "traefik.http.routers.ingest.entrypoints=websecure"
- "traefik.http.routers.ingest.tls.certresolver=letsencrypt"
- "traefik.http.services.ingest.loadbalancer.server.port=8001"
# No rate limiting - use backpressure signaling instead
# Only apply security headers (no rate-limit middleware)
- "traefik.http.routers.ingest.middlewares=security-headers@file"
networks:
- traefik-public
Middleware for Large Uploads:
# traefik/dynamic/middlewares.yml
http:
middlewares:
large-body:
buffering:
maxRequestBodyBytes: 10485760 # 10MB
memRequestBodyBytes: 2097152 # 2MB in memory
Pattern: Documentation Site with Caching¶
Problem: Static documentation site should be cached for performance.
Solution: Configure caching headers via middleware.
Implementation:
services:
docs:
image: squidfunk/mkdocs-material
command: serve --dev-addr 0.0.0.0:8080
volumes:
- ./docs:/docs/docs:ro
- ./mkdocs.yml:/docs/mkdocs.yml:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.docs.rule=Host(`docs.chvrm.flowcmd.io`)"
- "traefik.http.routers.docs.entrypoints=websecure"
- "traefik.http.routers.docs.tls.certresolver=letsencrypt"
- "traefik.http.services.docs.loadbalancer.server.port=8080"
- "traefik.http.routers.docs.middlewares=docs-cache@file"
networks:
- traefik-public
Caching Middleware:
# traefik/dynamic/middlewares.yml
http:
middlewares:
docs-cache:
headers:
customResponseHeaders:
Cache-Control: "public, max-age=3600" # 1 hour for HTML
Pattern: Traefik Dashboard (Internal Only)¶
Problem: Need monitoring dashboard but must restrict access.
Solution: Configure dashboard with basic auth, ideally on internal network only.
Implementation:
# docker-compose.yml - Traefik service
services:
traefik:
image: traefik:v3.2
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.chvrm.flowcmd.io`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=dashboard-auth@file"
Basic Auth Middleware:
# traefik/dynamic/middlewares.yml
http:
middlewares:
dashboard-auth:
basicAuth:
users:
# Generate with: htpasswd -nb admin password
- "admin:$apr1$xyz..."
When to Use: - Production monitoring - Debugging routing issues
Security Note: Consider restricting to VPN or internal network rather than public internet.
Anti-Patterns¶
❌ Anti-Pattern: Hardcoding Certificates¶
Problem: Manually managing SSL certificates leads to expiration and security issues.
Example of BAD configuration:
# DON'T DO THIS
services:
traefik:
volumes:
- ./certs/cert.pem:/certs/cert.pem
- ./certs/key.pem:/certs/key.pem
Why It's Wrong: - Certificates expire and require manual renewal - Private keys in repository is a security risk - No automatic rotation
Correct Approach:
# Use Let's Encrypt ACME
certificatesResolvers:
letsencrypt:
acme:
email: admin@chaverim.org
storage: /letsencrypt/acme.json
tlsChallenge: {}
❌ Anti-Pattern: Exposing Internal Services¶
Problem: Database and internal services accidentally exposed via Traefik.
Example of BAD configuration:
Why It's Wrong: - Database should never be internet-accessible - Massive security vulnerability - No authentication at network level
Correct Approach:
services:
postgres:
# No Traefik labels - internal only
networks:
- internal # Use separate network from Traefik
❌ Anti-Pattern: Missing Health Checks¶
Problem: Traefik routes to unhealthy containers, causing errors.
Example of BAD configuration:
Why It's Wrong: - Requests may route to crashed containers - No automatic recovery from failures
Correct Approach:
services:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
labels:
- "traefik.enable=true"
- "traefik.http.services.app.loadbalancer.healthcheck.path=/health"
- "traefik.http.services.app.loadbalancer.healthcheck.interval=30s"
Configuration¶
Environment Variables¶
| Variable | Required | Default | Description |
|---|---|---|---|
TRAEFIK_ACME_EMAIL |
Yes | - | Email for Let's Encrypt registration |
TRAEFIK_DOMAIN |
Yes | - | Base domain (e.g., chvrm.flowcmd.io) |
TRAEFIK_DASHBOARD_USER |
Yes | - | Dashboard username |
TRAEFIK_DASHBOARD_PASSWORD |
Yes | - | Dashboard password (htpasswd format) |
CF_DNS_API_TOKEN |
DNS-01 only | - | Cloudflare API token for DNS challenge |
Directory Structure¶
central/
├── docker-compose.yml
├── traefik/
│ ├── traefik.yml # Static configuration
│ ├── dynamic/
│ │ └── middlewares.yml # Dynamic middleware config
│ └── letsencrypt/ # Certificate storage (volume)
│ └── acme.json
└── mkdocs.yml # Documentation site config
Complete docker-compose.yml¶
Use with docker compose up -d (modern syntax). No version field needed.
services:
traefik:
image: traefik:v3.2
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN:-}
volumes:
- /var/run/docker.sock:ro
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/dynamic:/etc/traefik/dynamic:ro
- ./traefik/letsencrypt:/letsencrypt
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.${TRAEFIK_DOMAIN}`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=dashboard-auth@file"
networks:
- traefik-public
central-app:
image: alpr-central:latest
container_name: central-app
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
- DATABASE_URL=postgresql://...
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.${TRAEFIK_DOMAIN}`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=8000"
- "traefik.http.routers.app.middlewares=security-headers@file"
networks:
- traefik-public
- internal
ingest:
image: alpr-ingest:latest
container_name: ingest
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
interval: 30s
timeout: 10s
retries: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.ingest.rule=Host(`ingest.${TRAEFIK_DOMAIN}`)"
- "traefik.http.routers.ingest.entrypoints=websecure"
- "traefik.http.routers.ingest.tls.certresolver=letsencrypt"
- "traefik.http.services.ingest.loadbalancer.server.port=8001"
- "traefik.http.routers.ingest.middlewares=security-headers@file"
networks:
- traefik-public
- internal
docs:
image: squidfunk/mkdocs-material
container_name: docs
restart: unless-stopped
command: serve --dev-addr 0.0.0.0:8080
volumes:
- ./docs:/docs/docs:ro
- ./mkdocs.yml:/docs/mkdocs.yml:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.docs.rule=Host(`docs.${TRAEFIK_DOMAIN}`)"
- "traefik.http.routers.docs.entrypoints=websecure"
- "traefik.http.routers.docs.tls.certresolver=letsencrypt"
- "traefik.http.services.docs.loadbalancer.server.port=8080"
- "traefik.http.routers.docs.middlewares=docs-cache@file"
networks:
- traefik-public
postgres:
image: postgres:17
container_name: postgres
restart: unless-stopped
# NO traefik labels - internal only
networks:
- internal
networks:
traefik-public:
name: traefik-public
internal:
name: internal
Testing Strategies¶
Manual Testing Checklist¶
Set your domain variable first: export TRAEFIK_DOMAIN=chvrm.flowcmd.io
-
HTTP Redirect:
-
SSL Certificate:
-
Subdomain Routing:
-
Security Headers:
Let's Encrypt Staging¶
Always test with staging first:
certificatesResolvers:
letsencrypt:
acme:
caServer: https://acme-staging-v02.api.letsencrypt.org/directory
# ... rest of config
Switch to production after testing:
Common Issues & Solutions¶
Issue: Certificate Not Issued¶
Symptoms:
- Browser shows certificate error
- acme.json is empty or has errors
Cause: DNS not propagated, or port 443 blocked.
Solution:
# Check DNS resolution
dig app.chvrm.flowcmd.io
# Check port accessibility
nc -zv your-server-ip 443
# Check Traefik logs
docker logs traefik 2>&1 | grep -i acme
Issue: Service Not Discovered¶
Symptoms: - Traefik dashboard shows no routers - 404 errors on subdomain
Cause:
Missing labels, wrong network, or traefik.enable=false.
Solution:
# Verify service has correct labels
docker inspect central-app | grep -A50 Labels
# Verify service is on traefik network
docker network inspect traefik-public
# Check Traefik logs for discovery
docker logs traefik 2>&1 | grep -i "central-app"
Issue: WebSocket Connection Fails¶
Symptoms: - Real-time features don't work - WebSocket upgrade fails
Cause: Missing WebSocket headers in Traefik config.
Solution: Traefik v3 handles WebSocket automatically. If issues persist:
# traefik/dynamic/middlewares.yml
http:
middlewares:
websocket-headers:
headers:
customRequestHeaders:
Connection: "upgrade"
Upgrade: "websocket"
Related Documentation¶
Internal References¶
- Reverse Proxy Architecture - WHAT and WHY
- Documentation Site Architecture - Docs site design
- Global Patterns - Security patterns
External Resources¶
Quick Checklist¶
When implementing Traefik:
- [ ] Read this entire PRP
- [ ] Check
global.mdfor security patterns - [ ] Create DNS records for all subdomains
- [ ] Test with Let's Encrypt staging first
- [ ] Configure all services with health checks
- [ ] Apply security headers middleware
- [ ] Never expose database via Traefik
- [ ] Restrict dashboard access
- [ ] Test all subdomain routing
- [ ] Verify HTTPS redirect works
Remember: PRPs are living documents. Update as patterns evolve!