← Back to Wiki
Self-Hosting

Self-Hosting Guide

This is the "one-click" Docker deployment for your own Outpost server, plus the one genuine gotcha that will silently break voice chat if you skip it.

What you're standing up

Four containers: the Outpost app itself (API + web client, one image), Postgres, LiveKit for voice/video, and MinIO for file storage (avatars, attachments). The install script generates every secret and wires them together — nothing to hand-configure for a basic instance.

Requirements

Quick install

git clone https://github.com/blindrun/outpost-chat.git
cd outpost-chat/deploy
./install.sh

It asks for your server's public hostname/IP, a GitHub owner (to pull the pre-built image from ghcr.io), and whether to set up HTTPS — say yes unless this is purely LAN/local testing. It generates every secret and brings everything up with docker compose up -d.

A fresh instance prints a one-time claim code to its own log at startup (install.sh waits for and prints it directly, or grab it yourself with docker compose logs app). The client shows a "Claim This Server" prompt until someone enters that code — that's what actually creates the owner account. Nobody can register at all before that, so there's no race to become the first (real) user on a freshly exposed instance.

TLS is not optional — it's required for voice

Browsers only allow microphone access (getUserMedia) on a secure (HTTPS) page. That has a non-obvious knock-on effect: avatars/attachments (served by MinIO) and voice signaling (served by LiveKit) both need to be reachable from the same HTTPS origin as the app. Point them at a bare http://host:9000 or ws://host:7880 instead and it breaks silently in two different ways — browsers auto-upgrade insecure <img> requests to HTTPS and just fail with nothing listening there (broken avatar/icon images, no visible error), and they flatly refuse to even attempt an insecure WebSocket from a secure page (voice never connects — DOMException: The operation is insecure in the console, easy to mistake for a firewall/port problem when it isn't).

If you said yes to the HTTPS prompt, install.sh already generated a ready-to-use Caddyfile in the deploy/ directory that proxies both /outpost-uploads/* (MinIO) and /rtc/* (LiveKit signaling) through the same site as the app, and pointed LIVEKIT_URL/MINIO_PUBLIC_URL in .env at the matching wss:///https:// addresses. To put it into effect:

# Install Caddy first if you don't have it: https://caddyserver.com/docs/install
sudo cp Caddyfile /etc/caddy/Caddyfile
sudo systemctl reload caddy

Make sure your domain's DNS already points at this server before reloading — Caddy fetches a real Let's Encrypt certificate automatically on first request, no manual cert setup needed. A different reverse proxy (nginx, Traefik, whatever) works fine too — just replicate the same three routes (app, /outpost-uploads/* → MinIO, /rtc/* → LiveKit) under one HTTPS site.

Testing on a LAN with no domain? You can skip this — voice just won't work until you add TLS later (re-run install.sh after deleting .env to add it to an existing install).

Manual install, if you'd rather not run the script

cp .env.example .env
# edit .env: fill in JWT_SECRET, POSTGRES_PASSWORD, MINIO_ROOT_PASSWORD,
# LIVEKIT_API_KEY, LIVEKIT_API_SECRET (random strings -- `openssl rand -hex 32`
# works well), and set LIVEKIT_URL / MINIO_PUBLIC_URL to your real public
# host (see the TLS section above for which scheme/URL shape to use).

sed -e "s|__LIVEKIT_API_KEY__|<the key you just picked>|" \
    -e "s|__LIVEKIT_API_SECRET__|<the secret you just picked>|" \
    livekit.yaml.template > livekit.yaml

docker compose up -d

Building the image yourself instead of pulling it

Useful if you don't want a dependency on the pre-built ghcr.io image, or you're testing an unreleased change:

docker build -t outpost-chat:local -f ../Dockerfile ..
APP_IMAGE=outpost-chat:local docker compose up -d

Updating

docker compose pull
docker compose up -d

Database migrations run automatically on container start — no separate migration step.

Backups

Everything that matters lives in two named Docker volumes: outpost-pgdata (Postgres — accounts, messages, channels, roles) and outpost-minio (uploaded avatars/attachments). Back up both, same as you would any other stateful container.

Known limitation: uploaded files (avatars, attachments) sit in a public-read MinIO bucket — anyone with the direct URL can view them, there's no per-file access control. Fine for avatars and casual attachments; don't rely on this for anything sensitive.

Once your server is up, list it in the server directory so people can find it.