Skip to content

Self-hosting

A ZippChat instance is one public host name serving three things:

Path What
/ this website (optional for a private instance)
/app/ the web client
/api/…, /gateway, /media/… the backend

Voice signaling runs on livekit.<host>. Voice media needs real ports at your edge — 7881/tcp and 7882/udp — because WebRTC cannot cross an HTTP-only proxy or tunnel.

The web client has no domain compiled in: it talks to whatever origin served it, so the same image runs on your host and on the main service. The Android app can be pointed at any instance from its login screen.

Before you start, read the hosting model. On your own box, you are the operator who could be compelled to hand over anything the server can read — persistent channels, media in them, and all metadata.

What you run

Component Version in the reference deployment Notes
PostgreSQL 16 migrations run from the api image
Valkey (Redis protocol) 8 REDIS_URL must use the redis:// scheme
NATS with JetStream 2.10 event bus between services
Garage (S3-compatible) 1.0 sealed attachments; the variables are still named MINIO_*
Vault 1.17 channel and media KEKs — must be persistent, not -dev
LiveKit — voice SFU; media is end-to-end encrypted, it only forwards
api, keyserver, media, voice, worker — the five Go services
web client, site — static nginx images
reverse proxy — routes the paths above; most specific first

The proxy has to send /api/keys/ to the keyserver, /api/voice/ to voice, /api/media/ and /media/ to media, and the rest of /api/ plus /gateway (a WebSocket, with a long read timeout) to the api — before falling through to /app/ and /.

Vault must persist

If Vault loses its data, every channel KEK goes with it: all persistent channel history and every stored attachment become permanently undecryptable — while the API keeps answering 200 with empty channels. The development compose files run Vault in -dev mode (in memory) and are not deploy targets.

The supported path

The reference deployment is Kubernetes (k3s) on a single node, described in k3s deployment. The public host is set in exactly one place, Server/k8s/base/public-host.env:

PUBLIC_HOST=chat.example.org

Kustomize writes it into the Ingress (the main host and livekit.<host>), CORS_ALLOWED_ORIGINS and LIVEKIT_PUBLIC_URL. Nothing else names a domain.

Things a self-hoster has to provide that the manifests cannot:

  • LIVEKIT_NODE_IP — the public IP your edge advertises for voice media;
  • Vault initialised and unsealed (it does not auto-unseal; that is deliberate);
  • the secrets listed in the deployment guide (JWT secret, database password, LiveKit key pair, Garage RPC secret);
  • optionally, FCM_PROJECT_ID and FCM_CREDENTIALS_JSON for Firebase push. UnifiedPush works without any of it.

Accounts and other instances

An instance has its own accounts. Signing in to someone else's instance with a main-service account, and joining servers across instances, is designed (see the hosting model) but not built.