Reverse Proxy: nginx-proxy¶
This guide covers the nginx-proxy + acme-companion setup. If you prefer Traefik, see Reverse Proxy: Traefik instead.
LaSuite Meet uses a two-layer routing setup when using nginx-proxy:
- nginx-proxy (outer) - terminates TLS, routes by hostname, issues Let's Encrypt certs
- frontend container nginx (inner) - routes by URL path between the backend API and the React SPA
The inner routing nginx (nginx-routing.conf) is configured in Deployment Guide, Step 5 and applies regardless of which outer proxy you use. This page covers the outer nginx-proxy layer only.
Architecture¶
flowchart LR
B(["🌐 Browser"])
subgraph proxy_net["proxy network"]
NP["nginx-proxy\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| NP
NP -->|meet.example.com| FN
NP -->|livekit.example.com| LK
NP -->|auth.example.com| KC
FN -->|"/api /admin /oidc …"| BE
FN -->|"/media/"| MI
FN -->|"everything else"| SPA
Compose configuration¶
The frontend service needs to be on the proxy network (for nginx-proxy) and the internal network (to reach backend:8000):
frontend:
image: lasuite/meet-frontend:latest
entrypoint:
- /docker-entrypoint.sh
command: ["nginx", "-g", "daemon off;"]
environment:
- VIRTUAL_HOST=meet.example.com
- VIRTUAL_PORT=8083
- LETSENCRYPT_HOST=meet.example.com
- LETSENCRYPT_EMAIL=you@example.com
env_file: .env
volumes:
- ./nginx-routing.conf:/etc/nginx/conf.d/routing.conf:ro
depends_on:
- backend
networks:
- proxy
- internal
The backend service does not need to be on the proxy network. It is only accessed internally by the frontend nginx:
backend:
image: lasuite/meet-backend:latest
env_file: .env
depends_on:
- postgresql
- redis
networks:
- proxy
- internal # must be on proxy to reach Keycloak and LiveKit by public hostname
Keycloak also needs a virtual host¶
Keycloak must be accessible by the browser for the OIDC login redirect, so it gets its own subdomain via nginx-proxy:
keycloak:
environment:
VIRTUAL_HOST: auth.example.com
VIRTUAL_PORT: "8080"
LETSENCRYPT_HOST: auth.example.com
networks:
- proxy
- internal
LiveKit also needs a virtual host¶
LiveKit's WebSocket must be served over WSS (TLS). Give it its own subdomain via nginx-proxy:
livekit:
environment:
- VIRTUAL_HOST=livekit.example.com
- VIRTUAL_PORT=7880
- LETSENCRYPT_HOST=livekit.example.com
ports:
- "7881:7881"
- "7882:7882/udp"
networks:
- proxy
- internal
Port 7882/UDP must remain directly exposed - nginx cannot proxy UDP.
Three DNS records required¶
meet.example.com → <server-IP> (Meet)
auth.example.com → <server-IP> (Keycloak)
livekit.example.com → <server-IP> (LiveKit WebSocket)
Troubleshooting¶
502 on all routes: The frontend nginx is not listening on port 8083. Check docker logs <frontend-container> for errors. Verify nginx-routing.conf is mounted to /etc/nginx/conf.d/routing.conf:ro.
API returns 301 in a loop: X-Forwarded-Proto https is missing or wrong in the template. It must be hardcoded to https, not $scheme.
Login redirects to Keycloak but fails: Verify the backend container is on the proxy network so it can resolve auth.example.com and livekit.example.com.