Skip to content

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

  1. Docker Labels for Routing: Define routes via service labels
  2. Entrypoint Separation: HTTP (80) redirects to HTTPS (443)
  3. Let's Encrypt ACME: Automatic certificate management
  4. Middleware Chains: Compose security and rate limiting
  5. 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-app service
  • [ ] Add routing labels to ingest service
  • [ ] Add routing labels to docs service
  • [ ] 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:

# .env
CF_API_EMAIL=admin@chaverim.org
CF_DNS_API_TOKEN=your-cloudflare-api-token

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:

labels:
  - "traefik.http.routers.app.middlewares=security-headers@file"

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:

# traefik/traefik.yml
api:
  dashboard: true
  insecure: false  # Require authentication
# 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:

# DON'T DO THIS
services:
  postgres:
    labels:
      - "traefik.enable=true"  # NEVER expose database!

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:

services:
  app:
    labels:
      - "traefik.enable=true"
      # No health check configured

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

  1. HTTP Redirect:

    curl -I http://app.${TRAEFIK_DOMAIN}
    # Should return 301/308 redirect to HTTPS
    

  2. SSL Certificate:

    curl -vI https://app.${TRAEFIK_DOMAIN} 2>&1 | grep -A5 "Server certificate"
    # Should show Let's Encrypt certificate
    

  3. Subdomain Routing:

    curl -I https://app.${TRAEFIK_DOMAIN}    # Should reach central app
    curl -I https://ingest.${TRAEFIK_DOMAIN} # Should reach ingest
    curl -I https://docs.${TRAEFIK_DOMAIN}   # Should reach docs
    

  4. Security Headers:

    curl -I https://app.${TRAEFIK_DOMAIN} 2>&1 | grep -i strict-transport
    # Should return Strict-Transport-Security header
    

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:

      caServer: https://acme-v02.api.letsencrypt.org/directory

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"

Internal References

External Resources


Quick Checklist

When implementing Traefik:

  • [ ] Read this entire PRP
  • [ ] Check global.md for 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!