Skip to content

Reverse Proxy: Traefik

Traefik is a modern reverse proxy that integrates natively with Docker. Instead of configuring routes in separate files, services declare their own routing rules via container labels. Traefik picks them up automatically and issues TLS certificates via Let's Encrypt.

If you prefer nginx-proxy, see Reverse Proxy: nginx-proxy instead.

The frontend container also runs an internal nginx for path-based routing (API vs SPA vs MinIO). This is configured via nginx-routing.conf, created in Deployment Guide, Step 5. It applies regardless of which outer proxy you use. This page covers the outer Traefik layer only.

Architecture

flowchart LR
    B(["🌐 Browser"])

    subgraph proxy_net["proxy network"]
        T["Traefik\n:443 HTTPS"]
    end

    subgraph internal_net["internal network"]
        FN["frontend nginx\n:8083"]
        LK["LiveKit\n:7880"]
        KC["Keycloak\n:8080"]
        BE["Backend\n:8000"]
        MI["MinIO\n:9000"]
        SPA["React SPA\n:8080"]
    end

    B -->|HTTPS| T
    T -->|meet.example.com| FN
    T -->|livekit.example.com| LK
    T -->|auth.example.com| KC
    FN -->|"/api /admin /oidc …"| BE
    FN -->|"/media/"| MI
    FN -->|"everything else"| SPA

frontend, livekit, and keycloak are on both the proxy network (so Traefik can reach them) and the internal network (so they can reach backend services). All other services are on internal only.

Step 1: Create the proxy network

All services that Traefik routes traffic to must share a common Docker network. Create it once; it persists across container restarts:

docker network create proxy

Step 2: Create the acme.json file

Traefik stores Let's Encrypt certificates in a single file. Create it before starting Traefik. Traefik refuses to start if the file is missing or has incorrect permissions:

mkdir -p ~/docker/traefik
touch ~/docker/traefik/acme.json
chmod 600 ~/docker/traefik/acme.json

The file must be owned by the user running Traefik and have mode 600. Any other permission causes Traefik to exit with acme.json has too open permissions.

Step 3: Create the Traefik compose file

Create ~/docker/traefik/compose.yml:

services:

  traefik:
    image: traefik:v3.7
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./acme.json:/etc/traefik/acme/acme.json
    command:
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.network=proxy
      - --entrypoints.web.address=:80
      - --entrypoints.web.http.redirections.entryPoint.to=websecure
      - --entrypoints.web.http.redirections.entryPoint.scheme=https
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.letsencrypt.acme.email=you@example.com
      - --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json
      - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
      - --log.level=INFO
    networks:
      - proxy

networks:
  proxy:
    external: true

Replace you@example.com with your real email address. Let's Encrypt uses it for certificate expiry notifications.

Start Traefik:

cd ~/docker/traefik
docker compose up -d

How Meet services declare their routes

With Traefik, services use labels instead of environment variables for routing. Three Meet services need public access:

Service Hostname Internal port
frontend meet.example.com 8083
livekit livekit.example.com 7880
keycloak auth.example.com 8080

Each uses this label pattern:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.<name>.rule=Host(`<hostname>`)"
  - "traefik.http.routers.<name>.entrypoints=websecure"
  - "traefik.http.routers.<name>.tls=true"
  - "traefik.http.routers.<name>.tls.certresolver=letsencrypt"
  - "traefik.http.services.<name>.loadbalancer.server.port=<port>"

traefik.enable=true is required. The --providers.docker.exposedByDefault=false flag in the Traefik config means containers are invisible to Traefik unless they explicitly opt in with this label.

Services must be on the proxy network. Traefik connects to containers using the proxy network (set by --providers.docker.network=proxy). Any service Traefik routes to must have proxy in its networks: list in addition to the internal network.

Compose excerpt for Meet services

  frontend:
    image: lasuite/meet-frontend:latest
    # ...
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.meet-frontend.rule=Host(`meet.example.com`)"
      - "traefik.http.routers.meet-frontend.entrypoints=websecure"
      - "traefik.http.routers.meet-frontend.tls=true"
      - "traefik.http.routers.meet-frontend.tls.certresolver=letsencrypt"
      - "traefik.http.services.meet-frontend.loadbalancer.server.port=8083"
    networks:
      - proxy
      - internal

  livekit:
    image: livekit/livekit-server:latest
    # ...
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.meet-livekit.rule=Host(`livekit.example.com`)"
      - "traefik.http.routers.meet-livekit.entrypoints=websecure"
      - "traefik.http.routers.meet-livekit.tls=true"
      - "traefik.http.routers.meet-livekit.tls.certresolver=letsencrypt"
      - "traefik.http.services.meet-livekit.loadbalancer.server.port=7880"
    networks:
      - proxy
      - internal

  keycloak:
    image: quay.io/keycloak/keycloak:latest
    # ...
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.meet-keycloak.rule=Host(`auth.example.com`)"
      - "traefik.http.routers.meet-keycloak.entrypoints=websecure"
      - "traefik.http.routers.meet-keycloak.tls=true"
      - "traefik.http.routers.meet-keycloak.tls.certresolver=letsencrypt"
      - "traefik.http.services.meet-keycloak.loadbalancer.server.port=8080"
    networks:
      - proxy
      - internal

The complete compose.yml for Meet with Traefik is shown in the Deployment Guide.

WebSocket support for LiveKit

Traefik supports WebSocket upgrades automatically. The LiveKit WebSocket endpoint (wss://livekit.example.com) works without any additional configuration.

Verification

After starting the Meet stack, check that Traefik has discovered all three routes:

docker logs traefik 2>&1 | grep -i "Router\|certificate\|ACME" | tail -20

Verify TLS is working for each domain:

curl -s -o /dev/null -w "%{http_code}" https://meet.example.com/
curl -s -o /dev/null -w "%{http_code}" https://livekit.example.com/
curl -s -o /dev/null -w "%{http_code}" https://auth.example.com/
# All should return 200

Troubleshooting

Traefik refuses to start with "acme.json has too open permissions"

chmod 600 ~/docker/traefik/acme.json
docker compose restart traefik

502 Bad Gateway for a service

The container is not on the proxy network. Check the service's networks: list includes proxy:

networks:
  - proxy
  - internal

Recreate the container after fixing: docker compose up -d --force-recreate <service>

Route missing / service returns 404

traefik.enable=true label is missing from the container. Add it and recreate:

docker compose up -d --force-recreate <service>

Certificate not being issued

  • Port 80 must be publicly reachable (required for HTTP-01 ACME challenge)
  • DNS must resolve to the server before Traefik starts
  • Check logs: docker logs traefik | grep -i "acme\|certificate\|error"

Multiple Traefik instances / port conflict

If another process (nginx-proxy, Caddy, etc.) is already bound to port 80/443, Traefik will fail to start. Stop the conflicting service first.