Reverse Proxy & Routing¶
La Suite Meet uses a two-layer nginx setup:
- 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
Architecture¶
Browser
│
▼
nginx-proxy (TLS, port 443) ← routes by virtual host
│ visio.example.com → frontend:8083
▼
frontend container nginx (port 8083) ← routes by path
├── /api, /admin, /static, /oidc, /healthz, /webhook → backend:8000
└── everything else → frontend:8080 (SPA files)
How it works¶
The lasuite/meet-frontend Docker image contains:
- An nginx serving the built React SPA on port 8080 (default)
- A second nginx server block on port 8083 that handles routing
You provide the routing config by mounting a pre-rendered nginx config into /etc/nginx/conf.d/routing.conf. The frontend nginx picks it up on startup.
Note: The image supports a template mechanism (
/etc/nginx/templates/) but it requires a writable/etc/nginx/conf.d/directory at startup. On systems with Docker user namespace remapping, this fails with a permission error. Mounting a pre-rendered file directly bypasses this issue.
The routing nginx config¶
Create nginx-routing.conf in your meet directory:
upstream meet_backend {
server backend:8000 fail_timeout=0;
}
upstream meet_frontend {
server frontend:8080 fail_timeout=0;
}
server {
listen 8083;
server_name localhost;
charset utf-8;
server_tokens off;
client_max_body_size 100M;
location @proxy_to_meet_backend {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https; # must be hardcoded — Django uses this to skip SSL redirect
proxy_redirect off;
proxy_read_timeout 300s;
proxy_pass http://meet_backend;
}
location @proxy_to_meet_frontend {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://meet_frontend;
}
location / {
try_files $uri @proxy_to_meet_frontend;
}
location /api {
try_files $uri @proxy_to_meet_backend;
}
location /admin {
try_files $uri @proxy_to_meet_backend;
}
location /static {
try_files $uri @proxy_to_meet_backend;
}
location /oidc {
try_files $uri @proxy_to_meet_backend;
}
location /healthz {
try_files $uri @proxy_to_meet_backend;
}
location /webhook {
try_files $uri @proxy_to_meet_backend;
}
}
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=visio.example.com
- VIRTUAL_PORT=8083
- LETSENCRYPT_HOST=visio.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:
- internal # internal only — reached by frontend via Docker network
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
The backend needs extra_hosts to resolve Keycloak and LiveKit by their public hostnames (for token exchange and API calls):
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¶
visio.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. If you see /etc/nginx/conf.d is not writable, ensure you are mounting nginx-routing.conf directly to /etc/nginx/conf.d/routing.conf (not using the template path).
API returns 301 in a loop: X-Forwarded-Proto https is missing or wrong in nginx-routing.conf. It must be hardcoded to https, not $scheme.
Login redirects to Keycloak but fails: Check the extra_hosts entries on the backend container — it needs to resolve auth.example.com and livekit.example.com via host-gateway.