Skip to content

Self-Hosting

Run your own Hopp server. Includes PostgreSQL, Redis, LiveKit (WebRTC), and Caddy (auto-TLS).

  • A server with Docker and Docker Compose installed
  • A public hostname for your server. Either:
    • A real domain with A records for <DOMAIN> and livekit.<DOMAIN> (or wildcard *.<DOMAIN>) pointing to your server’s IP.
    • Or sslip.io for quick testing — use <your-ip>.sslip.io as DOMAIN (e.g. 51.15.42.10.sslip.io). Resolves automatically, no DNS setup. Subdomains work (livekit.<your-ip>.sslip.io). Real domain recommended for production.
  • Open firewall ports (see Firewall below)
Terminal window
git clone --depth 1 https://github.com/gethopp/hopp.git
cd hopp/selfhost
Terminal window
cp .env.example .env

Edit .env and set at minimum:

DOMAIN=hopp.example.com
SESSION_SECRET=<openssl rand -base64 32>
LIVEKIT_API_KEY=<openssl rand -base64 32>
LIVEKIT_API_SECRET=<openssl rand -base64 64>
POSTGRES_PASSWORD=<openssl rand -base64 24>

Generate secrets with the openssl commands shown.

Terminal window
docker compose up -d
Terminal window
curl https://hopp.example.com/api/health
# → OK

Open https://hopp.example.com in a browser and sign up. The first registered account becomes the team admin owner.

Hopp can sign users in through any OpenID Connect provider (Keycloak, Authentik, Pocket ID, …) in addition to the built-in login methods.

  1. Register a client at your provider with the redirect URI https://<DOMAIN>/api/auth/social/oidc/callback. Both confidential and public clients work; the authorization code flow always uses PKCE (S256).
  2. Set in .env:
OIDC_ISSUER_URL=https://id.example.com
OIDC_CLIENT_ID=<client id>
OIDC_CLIENT_SECRET=<client secret, leave empty for public clients>
OIDC_DISPLAY_NAME=Example SSO
  1. Restart the backend: docker compose up -d backend. The login page now shows a “Login with Example SSO” button. The desktop app signs in through the web app, so it works there too.

How accounts are handled:

  • Users are matched by email address. The provider must send email_verified: true, otherwise the login is rejected.
  • A new user without an invitation gets their own team, like with the other login methods. Set OIDC_SINGLE_TEAM=true if everyone from your provider belongs together: the first OIDC user creates the team and becomes its admin, everyone after that joins it. Teams that already exist are never joined this way. To use an existing team instead, mark it once before enabling the switch: UPDATE teams SET is_oidc_team = true WHERE id = <team id>;
  • The issuer and its endpoints must use https; plain http is accepted for localhost only. ID tokens are verified against the provider’s signing keys (jwks_uri).
  • If the provider cannot be reached when the backend starts, OIDC login is disabled with a warning in the log and the other login methods keep working. Restart the backend once the provider is back.

Many cloud providers (Scaleway DEV, Hetzner Cloud, basic DigitalOcean droplets) do not apply a firewall by default — these ports will already be reachable. Skip this section unless your provider has a security group, network ACL, or you’ve enabled ufw/firewalld on the host.

AWS, GCP, Azure, and Scaleway PRO/PROD instances typically need explicit security-group rules.

Open these ports if your provider applies a firewall:

PortProtocolPurpose
80TCPHTTP (TLS challenge)
443TCPHTTPS
7881TCPLiveKit TCP fallback
50000-60000UDPWebRTC media

Caddy handles TLS termination on ports 80/443. LiveKit media bypasses Caddy and goes directly to the server.

The web app served by your backend works without a rebuild — it derives the API URL from window.location at runtime.

For the desktop app, you can set a custom backend URL at runtime via Settings > Custom Backend URL (see Settings docs).

Web app: telemetry (PostHog analytics) is disabled by default in selfhosted builds. No data is sent to external services.

Desktop app: when connecting to a selfhosted backend, disable telemetry from the desktop app Settings.

Copy compose.override.example.yml to compose.override.yml and adjust port mappings.

If you already run nginx/traefik/caddy, skip the bundled Caddy:

  1. Remove the caddy service from compose.yml (or use an override)
  2. Point your proxy to backend:1926 (Docker DNS) and 127.0.0.1:7880 (LiveKit on host networking)
  3. Set USE_TLS=false (already the default)
  4. Set LIVEKIT_SERVER_URL=wss://livekit.yourdomain.com in .env

Local development (running selfhost stack on your machine)

Section titled “Local development (running selfhost stack on your machine)”

To run the self-hosted stack against localhost instead of a public domain:

The backend serves TLS directly when USE_TLS=true. Generate certs from the backend/ folder:

Terminal window
cd ../backend
task create-certs # uses mkcert; installs the local CA on first run

This produces backend/certs/localhost.pem and backend/certs/localhost-key.pem, which the override mounts into the backend container.

A ready-made compose.override.yml is committed for local use. It:

  • exposes backend:1926 directly on the host (skipping Caddy)
  • enables USE_TLS=true and mounts ../backend/certs
  • swaps LiveKit to livekit.dev.yaml (narrow UDP range 50000-50100, avoids macOS port-conflict storms)

Set .env:

DOMAIN=localhost
LIVEKIT_SERVER_URL=ws://localhost:7880
USE_TLS=true

Then:

Terminal window
docker compose up -d db cache livekit backend
# (skip caddy — not needed for localhost)

Backend reachable at https://localhost:1926.