Production deployment
End-to-end walkthrough: fresh VPS → working chatalot at https://chat.yourdomain.com in about 15 minutes.
This page is the canonical "how do I stand this up for real" path. It assumes you have:
- A Linux VPS (Debian 12+, Ubuntu 24.04+, or similar) with root or sudo access.
- A domain name where you can point an A/AAAA record at the VPS.
- Docker Engine and Compose v2 installed.
- A reverse proxy that can terminate TLS (Caddy, Traefik, or nginx). A minimal Caddy snippet is shown below if you don't have one yet.
If you only want a local-only dev instance or you're evaluating on your laptop, use the interactive installer instead — this page is tuned for production.
TL;DR — one-command install
Clone the repo on the VPS, then run:
./scripts/install.sh --non-interactive \
--domain chat.yourdomain.com \
--admin-username alice \
--registration invite_only \
--enable-turn --turn-external-ip YOUR.VPS.PUBLIC.IP \
--enable-updater \
--use-prebuilt
That writes a production-shaped configuration file, generates every required secret, seeds the update sidecar, and runs docker compose up -d with the right profiles. Final output includes a machine-parseable summary so an AI agent can pipe it forward:
public_url=https://chat.yourdomain.com
admin_username=alice
registration_mode=invite_only
turn_enabled=1
updater_enabled=1
compose_profiles=turn updater
What's still on you after the installer finishes: DNS, reverse proxy + TLS, firewall, and (if TURN is enabled) opening UDP/TCP 3478 and 5349.
Prerequisites
VPS sizing
| Scale | RAM | vCPU | Disk | Notes |
|---|---|---|---|---|
| Personal / small team | 1 GB | 1 | 20 GB | No TURN; voice/video may NAT-traverse. |
| Team + voice | 2 GB | 2 | 30 GB | TURN required if users are behind restrictive NAT. |
| Production workload | 4 GB+ | 2+ | 60 GB+ | Headroom for file uploads + pg_dump snapshots. |
Packages
# Debian / Ubuntu
sudo apt update && sudo apt install -y docker.io docker-compose-v2 openssl curl git
# Fedora / RHEL
sudo dnf install -y docker docker-compose openssl curl git
sudo systemctl enable --now docker
# Arch
sudo pacman -S docker docker-compose openssl curl git
sudo systemctl enable --now docker
DNS
Point an A (and ideally AAAA) record at the VPS public IP before you start the installer. The PUBLIC_URL and OIDC redirect URIs are baked into the configuration file; changing the domain later is a one-file edit plus a docker compose up -d to pick it up.
Install
1. Clone the repo
(The path doesn't matter; /srv/chatalot is convention. The installer runs relative to the repo root.)
2. Run the installer
Minimal production invocation:
./scripts/install.sh --non-interactive \
--domain chat.yourdomain.com \
--admin-username alice \
--registration invite_only \
--use-prebuilt
Add features as needed:
- Voice/video:
--enable-turn --turn-external-ip <public IPv4>. The installer generates the TURN auth secret and writes the TURN URLs into the configuration file. You still need to open UDP/TCP 3478 and 5349 on your firewall. - Managed updates:
--enable-updater. Runs thechatalot-updatersidecar so the admin panel can pull signed releases from the official registry. Covered in depth atdocs/admin-guide/updater.md. - Authenticated image pulls (required when pulling from a private registry):
--registry-user <name> --registry-token <token>. Writessecrets/registry_credswith the credentials. If your registry is public, omit these. - OIDC/SSO:
--oidc-issuer <issuer-URL> --oidc-client-id <id> --oidc-client-secret <secret>. Add--oidc-disable-password-loginto force OIDC-only logins.
Run ./scripts/install.sh --help for the full flag list.
3. Put a reverse proxy in front
The installer does NOT install or configure your reverse proxy — that's an operator choice and varies too much to script safely. Chatalot listens on 127.0.0.1:8080 by default; your proxy terminates TLS at :443 and forwards to it. WebSocket upgrades at /ws are required for real-time features.
A minimal Caddyfile:
chat.yourdomain.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8080
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
-Server
}
}
Caddy auto-provisions a Let's Encrypt cert on first request. If you prefer Traefik or nginx, any proxy that correctly handles WebSocket upgrades works; see cloudflare-tunnel.md for the tunnel-based alternative.
4. Verify
# Local health endpoint
curl -sf http://127.0.0.1:8080/api/health | jq
# Public health endpoint (once DNS + TLS are live)
curl -sf https://chat.yourdomain.com/api/health | jq
Expected response:
Then open https://chat.yourdomain.com in a browser and register the admin user matching --admin-username. The first user to register under that name gets admin; subsequent registrations under the same name are treated as an attempt to log in.
Firewall
| Port | Protocol | Purpose | Required? |
|---|---|---|---|
| 80, 443 | TCP | Reverse proxy (HTTP/HTTPS) | Always |
| 3478 | UDP + TCP | TURN (plaintext) | With --enable-turn |
| 5349 | UDP + TCP | TURN over TLS/DTLS | With --enable-turn |
The chatalot backend port (8080) should NOT be exposed directly to the public internet — bind it to localhost and let the reverse proxy gate it.
Post-install: operator tasks
- Back up
secrets/. The installer generates one-of-a-kind cryptographic material there (JWT private key, TOTP encryption key, TURN auth secret, updater HMAC token). Losing these means existing sessions, TOTP codes, and managed-update HMAC will all break. Offline copy, encrypted backup, password manager — your pick, but somewhere not on the same VPS. - Replace the seeded
cosign_pubif you're using managed updates. The installer seedssecrets/cosign_pubfrom the example file so the updater sidecar can start. For production you want the real public key corresponding to the signing channel you trust. See../admin-guide/updater.mdfor the canonical Seglamater pubkey + fingerprint. - Rotate
registry_credson a schedule if you're pulling authenticated. The installer writes the value you passed in verbatim; regenerating on a cadence (e.g., at key-rotation time for your registry) is just a matter of rewriting the file anddocker compose restart chatalot-updater. - Turn on backups. Managed updates take their own pre-apply snapshot, but an independent backup of postgres + uploaded files is non-negotiable. See
backup-and-restore.md.
Managed updates in one minute
With --enable-updater, the admin-facing Updates panel polls the configured release channel, shows available versions, and (per the CHAT-13 vendor/client split) requires an explicit admin click to apply. An apply:
- Pulls the new image (cosign-verified against the public key).
- Snapshots the database.
- Runs the release's
pre_flight.shin a throwaway container. - Stops the old container, runs
migrate.sh, starts the new one. - Waits for the new container's health check; on failure, restores the snapshot and recreates the old container.
See ../admin-guide/updater.md for the full flow and the HMAC/token model.
Troubleshooting
./scripts/install.sh: permission denied
Health endpoint returns 502 through the proxy but 200 on localhost
Reverse proxy isn't forwarding WebSocket upgrades, or port 8080 is bound to 0.0.0.0 and firewalled. Check the proxy logs first; the WS path is /ws.
docker compose up -d fails with "secret file not found"
An expected secret file is missing. The installer creates them all; if you ran with a custom --data-dir or edited the compose file by hand, make sure every referenced secret exists:
ls -la secrets/
# expected for a full install:
# jwt_private.pem jwt_public.pem db_password totp_encryption_key
# updater_token cosign_pub registry_creds # when --enable-updater
TURN works on LAN but fails from outside
Firewall / NAT. TURN needs UDP 3478 + 5349 (and TCP for TURN-over-TCP fallback) forwarded to the VPS. TURN_EXTERNAL_IP must match the public address clients resolve to; using a private 10.x / 192.168.x address here is the single most common misconfiguration.
Admin account stuck
ADMIN_USERNAME is honored at first-launch bootstrap when there are zero admins. If an admin already exists, the value is ignored silently. To promote an existing user, use the admin panel (logged in as another admin), or hit the database directly:
docker compose exec postgres psql -U chatalot -c "UPDATE users SET is_admin=true WHERE username='alice';"
Next steps
- Admin guide — managed updates
- Backup and restore
- Configuration reference — every environment variable the server honors.