Deployment tutorial
A step-by-step path from an empty host to a working Lag instance. Each step ends with a check you can run. Provider registration is covered in Prism, OIDC, and OAuth; backups, upgrades, and troubleshooting stay in Operations.
Step 1: Prerequisites
- A Linux host with Docker Engine and the Compose plugin.
- An identity provider registration (Prism, OIDC, or OAuth 2.0), or a deliberate decision to enable nickname guests.
- Ports reachable by clients: TCP
7880-7881and UDP50000-50200. Port3000is the web entry point. - A public address for
EXTERNAL_IP, and a domain plus TLS certificate before you expose the instance to the internet.
Step 2: Get the files
git clone https://github.com/wyf9/lag-server.git
cd lag-server
cp .env.example .env.env.example is an insecure localhost example. Treat every value in it as a placeholder.
Step 3: Edit .env
Set at least these values:
| Variable | What to put there |
|---|---|
ALLOWED_HOSTS | The exact browser origin, e.g. https://lag.example.com, no path or trailing slash |
AUTH_PROVIDER | prism, oidc, or oauth2, plus that provider's variables |
AUTH_CLIENT_ID / AUTH_CLIENT_SECRET | Credentials from the provider registration |
GUEST_ENABLED | false unless nickname guests are intentional |
LAG_VOICE_KEY / LAG_VOICE_SECRET | Fresh random values, e.g. openssl rand -hex 32 for each |
EXTERNAL_IP | The address clients can reach |
VOICE_URL | The signaling URL clients will use; must be reachable from browsers |
LAG_BIND_ADDRESS | Defaults to 127.0.0.1; use 0.0.0.0 when clients connect to this host directly |
# Example check before starting
grep -E '^(ALLOWED_HOSTS|AUTH_PROVIDER|EXTERNAL_IP|VOICE_URL|LAG_BIND_ADDRESS)=' .envStep 4: Start the stack
compose.yml pulls ghcr.io/wyf9/lag-server:latest. Pin a release tag or digest for anything you cannot casually rebuild:
docker compose up -d
docker compose ps
curl --fail http://localhost:3000/api/health/api/health reports application process health, not full PostgreSQL or LiveKit readiness. PostgreSQL data lives in the named lag_data volume; keep it on durable storage.
Step 5: Verify the install
- Open the origin from Step 3 in a browser and sign in through the provider (or as a guest if enabled).
- Confirm the first external identity received the bootstrap
platform_admingrant, then audit it (see Permissions). - Create a room, send a message, and reconnect after a page reload to confirm sessions persist.
Do this from a network other than the server's LAN before you continue.
Step 6: Open the voice ports
| Port | Protocol | Exposure | Purpose |
|---|---|---|---|
3000 | TCP | Public or behind HTTPS proxy | Web UI, REST API, /api/ws |
3001 | TCP | Internal only | Fastify API |
3002 | TCP | Internal only | SvelteKit server |
5432 | TCP | Internal only | Bundled PostgreSQL |
7880 | TCP | Client reachable | LiveKit signaling/API |
7881 | TCP | Client reachable | LiveKit RTC TCP fallback |
50000-50200 | UDP | Client reachable | LiveKit RTC media |
Forward TCP 7880-7881 and the full UDP range 50000-50200 to the container host, and keep 3001, 3002, and 5432 private. Example with ufw:
sudo ufw allow 7880/tcp
sudo ufw allow 7881/tcp
sudo ufw allow 50000:50200/udpDo not map only HTTP: signaling can succeed while media stays silent if UDP is blocked. Carrier-grade NAT or restrictive corporate networks may need a TURN design, which this repository does not bundle or document as implemented.
Check: an off-network client can join a voice room and be heard.
Step 7: Serve HTTPS
Terminate HTTPS for port 3000, including WebSocket upgrades for /api/ws, with long enough idle timeouts for WebSockets. Example nginx server block:
server {
listen 443 ssl;
server_name lag.example.com;
# ssl_certificate /path/to/fullchain.pem;
# ssl_certificate_key /path/to/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}Then:
- Set
PROXY_HEADER=x-forwarded-foronly when the edge overwrites that header as shown above; otherwise leave it unset. - Update
ALLOWED_HOSTSto the HTTPS origin and restart:docker compose up -d. - Do not expose
3001as a second public API path. - Make the voice signaling URL reachable over TLS as well: served over HTTPS, browsers block insecure
ws://mixed content, soVOICE_URLmust be awss://URL clients can reach. The bundled defaults are development-oriented and this repository does not ship that proxy configuration.
Check: curl --fail https://lag.example.com/api/health succeeds, login works, and a browser console outside the LAN shows no mixed-content errors.
Step 8 (optional): Build from local source
To build the checked-out source instead of pulling the published image, rename compose-build.override.yml to compose.override.yml. Docker Compose merges it automatically:
mv compose-build.override.yml compose.override.yml
docker compose up -d --buildRemove compose.override.yml to go back to the published image.
Before you go public
- Replace every development value in
.env; the checked-in Compose files are not a complete production configuration. - Register the callback
https://your-origin/api/auth/callbackexactly, and read Authentication before exposing the instance. - Validate the exact public URL, certificates, signaling path, and media ports from an off-site browser.
- Record a backup and upgrade plan from Operations.
Scaling limits
Do not run multiple all-in-one replicas against copied volumes. WebSocket presence is in memory, PostgreSQL is bundled, and LiveKit node routing is not coordinated. High availability or horizontal scaling requires an architectural change and deployment-specific testing.