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
proxynetwork (so Traefik can reach them) and theinternalnetwork (so they can reach backend services). All other services are oninternalonly.
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:
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:
The file must be owned by the user running Traefik and have mode
600. Any other permission causes Traefik to exit withacme.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:
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=trueis required. The--providers.docker.exposedByDefault=falseflag in the Traefik config means containers are invisible to Traefik unless they explicitly opt in with this label.Services must be on the
proxynetwork. Traefik connects to containers using theproxynetwork (set by--providers.docker.network=proxy). Any service Traefik routes to must haveproxyin itsnetworks: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:
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"
502 Bad Gateway for a service
The container is not on the proxy network. Check the service's networks: list includes proxy:
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:
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.