Self-hosting

thinge is designed to run on a single server you control. This page is the practical recipe: one Linux box, rootless podman, the thinge container image, and public TLS. It works on a cloud VPS or a dedicated machine; only the firewall layer differs.

Voice media is UDP straight to the server — it never passes through a reverse proxy. That is why there is no nginx or Caddy in this stack: the server terminates TLS itself, and the media wants the raw host.

What runs

Piece What it is Ports
thinge server the app image — HTTP, TLS, and the voice SFU 80 (redirect), 443 (TLS), UDP for voice
PostgreSQL the database, bound to localhost 127.0.0.1:5432
coturn a TURN relay, for clients behind restrictive networks 3478, 5349, a UDP relay range
certificate tool issues/renews TLS via ACME HTTP-01 80 (briefly)

1. Provision the box

  • Ubuntu 22.04 / 24.04 (or similar), SSH keys only, automatic security updates.
  • A DNS A record: chat.example.com → <your public IP>. HTTP-01 certificates need only the record — no DNS API access.
  • Open the firewall:
    tcp 22 (your IPs only), 80, 443, 3478, 5349
    udp 3478, 50000-50249 (voice), plus the coturn relay range
  • Install podman, create an unprivileged thinge user, and enable lingering so its containers survive logout.

No Docker. Containers are podman-only.

2. Build the image

There is no public registry yet — build on the box from a source copy (copy the repository up with rsync; no git credentials needed on the server):

rsync -a --exclude target --exclude node_modules --exclude _build --exclude deps \
./thinge/ thinge@chat.example.com:thinge/
ssh thinge@chat.example.com \
'podman build -f thinge/server/Containerfile -t thinge-server:latest thinge/server'

The image is per-architecture — build it on (or for) the box's architecture.

3. Get a TLS certificate

Web push and modern browsers require a trusted certificate. Any ACME client works; the simplest is a standalone HTTP-01 issuance on port 80 (free while the app listens on 443), renewed on a daily timer that restarts the server so it picks up the new certificate.

4. Configure (environment)

thinge is entirely environment-driven — no config files to edit. Put these in an env file the container reads:

DATABASE_URL=ecto://postgres:<password>@127.0.0.1:5432/thinge
SECRET_KEY_BASE=<a long random secret>
PHX_HOST=chat.example.com
PORT=4000
TLS_PORT=443
TLS_CERT_PATH=/path/to/fullchain.crt
TLS_KEY_PATH=/path/to/private.key
# Allowed origins (add the desktop app's origins so it can health-check you):
PHX_CHECK_ORIGIN=https://chat.example.com,tauri://localhost,http://tauri.localhost
CORS_ORIGINS=https://chat.example.com,tauri://localhost,http://tauri.localhost
# Bounded UDP range for voice (must match the firewall rule):
ICE_PORT_RANGE=50000-50249
# TURN relay (coturn on the same box):
TURN_URLS=turn:chat.example.com:3478?transport=udp,turns:chat.example.com:5349
TURN_STATIC_AUTH_SECRET=<a random secret, shared with coturn>

Notifications (optional)

To enable web push, generate a VAPID key pair once and set:

VAPID_PUBLIC_KEY=<public key>
VAPID_PRIVATE_KEY=<private key>
VAPID_SUBJECT=mailto:admin@chat.example.com

Leave these unset and push is simply disabled; everything else works. (Outbound email — for invites and digests — is not yet available.)

Topic naming (optional)

Topics get a name from their opening message by default. To have an LLM write better titles, an admin points thinge at any OpenAI-compatible chat endpoint in Server settings → Topic naming (endpoint URL, optional API key, model) — a self-hosted model (e.g. llama.cpp) or a cloud gateway. It's configured in the app, not the environment, and is off until you set a URL; with it off, no message text ever leaves your server for titling.

A note on security

On a public box, leave GitHub OAuth unset unless you want anyone with a GitHub account to self-provision. With it unset, the server is invite-only (email/password), and anonymous sessions are disabled by default in production.

5. Run it

Start PostgreSQL, run the database migrations once before the first server start, then start the server and coturn — all as rootless podman containers, persisted as user services so they come back after a reboot. The server runs on the host network so voice UDP and the addresses it advertises are the real host.

6. First run

  • Visit https://chat.example.com. While the user list is empty, the login page offers bootstrap registration — the first account becomes the admin. After that it is invite-only.
  • Create channels from the admin UI, and smoke-test voice from two browsers.

Sizing

The server forwards audio — it never decodes, mixes, or transcribes (those are done on the clients). A small team's voice, chat, and database fit comfortably in 2 vCPU / 4 GB. Screen-share relaying adds bandwidth, not much CPU. Dedicated hardware only becomes interesting with many concurrent rooms or viewers.