Reverse proxy & TLS ​
Refract.Chat doesn't terminate TLS itself. Put a reverse proxy such as Traefik, Caddy or nginx in front of it, with two host names:
| Host name | Proxies to | Carries |
|---|---|---|
chat.example.com | backend:8778 (HTTP) | The web app, the API, Socket.IO, media, activities and federation |
livekit.chat.example.com | livekit:7866 (HTTP) | LiveKit's signalling, over WebSocket |
Both need WebSocket support: Socket.IO lives at /api/socket.io, and LiveKit signals over a WebSocket.
Ports ​
| Port | Service | Publish it? |
|---|---|---|
8778/tcp | backend | Only through the proxy, with TLS |
8779/tcp | backend | Never. LiveKit's webhooks arrive here, and only LiveKit should reach it, over the Docker network |
7866/tcp | LiveKit | Only through the proxy, with TLS |
7867/udp | LiveKit | Directly, not through the proxy: call media |
7868/tcp | LiveKit | Directly, not through the proxy: call media when UDP is blocked |
The media ports must match LiveKit's own configuration. See Voice & LiveKit to change them.
Traefik ​
docker-compose.override.example.yaml has working Traefik labels for both services. Copy it to docker-compose.override.yaml and change the host names, the entry point (https) and the certificate resolver (refract_chat) to your Traefik's. Both containers join the external traefik-net network so Traefik can reach them.
nginx ​
The same routes for nginx, with your own certificates:
nginx
map $http_upgrade $connection_upgrade {
default upgrade;
"" close;
}
server {
listen 443 ssl;
server_name chat.example.com;
# allow the instance's file size limit (100 MB unless an admin changed it)
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:8778;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl;
server_name livekit.chat.example.com;
location / {
proxy_pass http://127.0.0.1:7866;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}This assumes nginx runs on the host, so publish 8778 and 7866 on 127.0.0.1 only, in your docker-compose.override.yaml.
Upload size ​
The proxy must accept files up to the instance's limit, which is 100 MB unless an admin changes it under Manage instance → Config → Instance features. A proxy with a lower limit wins, and uploads above it fail before they reach Refract.
Client IP addresses ​
Behind a proxy, every request seems to come from the proxy, so rate limits and IP bans would apply to everyone at once. Tell the server which proxy to trust, in .env:
dotenv
TRUST_PROXY=true
TRUST_PROXY_IPS=172.20.0.0/16TRUST_PROXY_IPS takes addresses or CIDR ranges, comma-separated. A container's address can change when it's recreated, so trusting the proxy's Docker network is simplest. For Traefik, that's the traefik-net subnet:
sh
docker network inspect traefik-net --format '{{(index .IPAM.Config 0).Subnet}}'Only trust a network that nothing else can use to reach the server directly: anything trusted can claim to be any client. The server reads X-Forwarded-For (or TRUST_PROXY_HEADER, if set), and walks it from the right past every trusted hop to the real client. Its startup log shows what it trusts in a Client IPs: line, and it warns once for each untrusted peer that sends a forwarded header.
Several server processes ​
One server container runs one API process, which needs nothing special. If you run several behind a load balancer, they must share the database, Redis and JWT_SECRET, and the load balancer needs sticky sessions, because Socket.IO falls back to HTTP long-polling, which must keep hitting the same process.