- The browser talks to three separate origins, so each one needs its own hostname. There is no single-port configuration.
- The Convex client keeps a WebSocket open to the backend. A proxy that does not forward the upgrade headers leaves you with an app that loads, renders, and never shows any data.
./install.sh once and have a working install on 127.0.0.1.
The three origins
The dashboard on
6791 is a fourth, and it is an admin tool holding a key to your entire database. Leave it on localhost and reach it over an SSH tunnel unless you have a reason not to.
Set it up
1
Point three hostnames at the machine
Create three A (or CNAME) records. Any names will do; these are the ones used throughout this page:
If you are on a LAN with no public DNS, three entries in your router’s DNS or in
/etc/hosts work the same way — but you will be on plain HTTP, and browsers refuse mixed content, so keep all three on HTTP together or all three on HTTPS together. Never one on each.2
Configure the proxy
- Caddy
- nginx
- Traefik
Caddy handles the WebSocket upgrade and the certificates itself, which is why it is the short one:
Caddyfile
3
Tell OneRep its own addresses
The app bundle is static, so its backend URLs are compiled in at build time. Edit No trailing slashes. These three values have to match the hostnames in your proxy config character for character — they end up in the CORS allowlist, and a stray
selfhost/.env:selfhost/.env
www or a http where you meant https is a rejected request.4
Re-run the installer
docker compose up -d --build is not a substitute. The installer does two things nothing else does: it rebuilds the app image against the new origins, and it pushes SITE_URL to the backend so your new domain is trusted. Skip it and you get a bundle pointing at the right place talking to a backend that has never heard of it.Your data and secrets survive. Re-running is idempotent.Check your work
From any machine that can reach the proxy:HTTP/1.1 101 Switching Protocols. Anything else — a 400, a 404, a 502 — means the app will hang, and no amount of configuration elsewhere will save it.
Then open the app, sign in, and watch the browser console. A clean install logs no errors.
When it does not work
The app loads but stays empty, or spins forever
The app loads but stays empty, or spins forever
The WebSocket is not getting through. Run the upgrade check above; if it returns
400 InvalidConnectionHeader — Connection header did not include upgrade, your proxy is dropping the Upgrade and Connection headers. On nginx that is the missing map block and proxy_http_version 1.1. On Apache it is mod_proxy_wstunnel, unloaded by default.A 404 instead means the proxy is rewriting the path — check for a trailing slash on proxy_pass, which makes nginx strip the location prefix.CORS errors, or sign-in fails with no obvious reason
CORS errors, or sign-in fails with no obvious reason
The backend allows exactly the origins it was told about, and it learns your app’s origin from If it is wrong, re-running Lost the admin key? Regenerate one with
SITE_URL. If the browser console says the request was blocked by CORS policy, SITE_URL on the backend does not match APP_URL in selfhost/.env.Almost always this is a .env edit that was never followed by ./install.sh. Check what the backend actually believes. The Convex CLI needs to be told where your backend is and handed the admin key the installer printed:./install.sh fixes it along with everything else. To set it by hand:docker compose exec backend ./generate_admin_key.sh.Sign-in bounces you to 127.0.0.1
Sign-in bounces you to 127.0.0.1
Same cause, different symptom. Sign-in redirects resolve against
SITE_URL, so a stale value sends you back to localhost after the login succeeds. Fix SITE_URL as above.Mixed content warnings, or requests blocked in the console
Mixed content warnings, or requests blocked in the console
The app is on HTTPS and at least one Convex origin is still on HTTP. Browsers refuse to let a secure page open an insecure connection, including
ws://. All three origins have to be on the same scheme. Check what got baked into the bundle — view source on the app and look for the origins, or just re-read selfhost/.env and confirm every one of the three says https.It works for a minute, then disconnects and reconnects forever
It works for a minute, then disconnects and reconnects forever
Your proxy is timing out the idle WebSocket. nginx defaults
proxy_read_timeout to 60 seconds. Raise it to an hour on the convex. server block. Cloudflare’s proxy has its own idle timeout you cannot raise; it will reconnect rather than break, but expect a blink.Can I serve all of it from one hostname?
Can I serve all of it from one hostname?
No. The Convex origins have to be bare origins, so a path prefix such as
/convex will not work, and the two Convex services cannot share a hostname with each other either. Three names is the supported shape. They are free.Behind Cloudflare or a tunnel
A Cloudflare Tunnel works, with two notes. WebSockets must be enabled on the zone (they are by default on every plan). And the orange-cloud proxy imposes its own request size and idle limits, so large photo uploads and long-idle sockets behave slightly differently than they do on a plain origin. Map the three hostnames to the three local ports exactly as above; nothing else changes.Once the app is reachable on a real hostname over TLS, it installs as a PWA from the browser and behaves like the native builds. See Mobile apps.