Skip to content

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-7881 and UDP 50000-50200. Port 3000 is 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 ​

bash
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:

VariableWhat to put there
ALLOWED_HOSTSThe exact browser origin, e.g. https://lag.example.com, no path or trailing slash
AUTH_PROVIDERprism, oidc, or oauth2, plus that provider's variables
AUTH_CLIENT_ID / AUTH_CLIENT_SECRETCredentials from the provider registration
GUEST_ENABLEDfalse unless nickname guests are intentional
LAG_VOICE_KEY / LAG_VOICE_SECRETFresh random values, e.g. openssl rand -hex 32 for each
EXTERNAL_IPThe address clients can reach
VOICE_URLThe signaling URL clients will use; must be reachable from browsers
LAG_BIND_ADDRESSDefaults to 127.0.0.1; use 0.0.0.0 when clients connect to this host directly
bash
# Example check before starting
grep -E '^(ALLOWED_HOSTS|AUTH_PROVIDER|EXTERNAL_IP|VOICE_URL|LAG_BIND_ADDRESS)=' .env

Step 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:

bash
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 ​

  1. Open the origin from Step 3 in a browser and sign in through the provider (or as a guest if enabled).
  2. Confirm the first external identity received the bootstrap platform_admin grant, then audit it (see Permissions).
  3. 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 ​

PortProtocolExposurePurpose
3000TCPPublic or behind HTTPS proxyWeb UI, REST API, /api/ws
3001TCPInternal onlyFastify API
3002TCPInternal onlySvelteKit server
5432TCPInternal onlyBundled PostgreSQL
7880TCPClient reachableLiveKit signaling/API
7881TCPClient reachableLiveKit RTC TCP fallback
50000-50200UDPClient reachableLiveKit 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:

bash
sudo ufw allow 7880/tcp
sudo ufw allow 7881/tcp
sudo ufw allow 50000:50200/udp

Do 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:

nginx
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:

  1. Set PROXY_HEADER=x-forwarded-for only when the edge overwrites that header as shown above; otherwise leave it unset.
  2. Update ALLOWED_HOSTS to the HTTPS origin and restart: docker compose up -d.
  3. Do not expose 3001 as a second public API path.
  4. Make the voice signaling URL reachable over TLS as well: served over HTTPS, browsers block insecure ws:// mixed content, so VOICE_URL must be a wss:// 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:

bash
mv compose-build.override.yml compose.override.yml
docker compose up -d --build

Remove 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/callback exactly, 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.

Self-hosted voice and chat: voice rooms, text chat, and room roles on your own server.