← Back to Wiki
Self-Hosting

Self-Hosting Guide

Stand up your own Outpost server with Docker. Read the TLS section before you start. Skip it and voice chat breaks in a way that looks like a firewall problem and is not.

What you are standing up

Four containers.

The install script generates every secret and wires them together. A basic instance needs nothing hand-edited.

One server is one community. Outpost is not federated.

The usual question first. Can your server talk to someone else's, the way Matrix and Mastodon do, so one account works everywhere? No. That is a decision, not a gap.

Every instance is its own community with its own accounts. Join two Outpost servers and you have two accounts. The clients keep a list and switch between them. Your members, messages and files never leave your server.

Be honest about both halves of that trade. You lose one login everywhere, and you lose DMs across servers.

What you get back is a server that depends on nobody. No shared trust to withdraw. No other operator's moderation or downtime landing on your members. No identity server to keep alive. Much less to secure and back up.

That fits what Outpost is for. A server you run for people you know, not a node in a public network.

Federation is not on the roadmap. It is not a feature you bolt on. It means rebuilding the account model from the bottom and signing up to a protocol forever. Want a federated network? Matrix already does that well. Use it rather than wait on this.

What you need

The LiveKit ports are fixed. It runs on Docker host networking instead of published ports, because pushing ten thousand UDP ports through Docker's iptables mapping is slow enough to hang docker compose up outright.

Quick install

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

It asks three things. Your server's public hostname or IP. A GitHub owner, so it knows which prebuilt image to pull from ghcr.io. And whether you want HTTPS. Say yes to that unless this is LAN testing.

Then it generates every secret and brings the stack up.

A fresh instance prints a one-time claim code to its log. install.sh waits and shows it to you. Read it yourself with docker compose logs app if you missed it.

Enter that code at the Claim This Server prompt. That creates the owner account. Nobody can register before it happens, so an exposed server cannot be claimed out from under you.

TLS is required, because voice needs it

BE WARNED: browsers only give a page the microphone over HTTPS. That drags two other things along with it. MinIO and LiveKit both have to be reachable from the same HTTPS origin as the app. Point them at a bare http://host:9000 or ws://host:7880 and it breaks twice, quietly. Images get auto-upgraded to HTTPS, find nothing listening, and your avatars just do not appear with no error anywhere. And the browser flatly refuses an insecure WebSocket from a secure page, so voice never connects at all. The console says DOMException: The operation is insecure, which reads exactly like a blocked port and is not one.

Said yes to HTTPS? Then install.sh already wrote a Caddyfile into deploy/ for you. It proxies /outpost-uploads/* to MinIO and /rtc/* to LiveKit through the same site as the app, and it pointed LIVEKIT_URL and MINIO_PUBLIC_URL at the matching wss:// and https:// addresses.

Put it in place with these.

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

Point your domain's DNS at this server before you reload. Caddy fetches a real Let's Encrypt certificate on the first request. There is no certificate step to do by hand.

Any other reverse proxy works. nginx, Traefik, whatever you already run. Recreate the same three routes under one HTTPS site. The app, /outpost-uploads/* to MinIO, /rtc/* to LiveKit.

On a LAN with no domain, skip all of it. Voice will not work until you add TLS. Delete .env and re-run install.sh when you are ready to.

Signing in with your own identity provider

Optional. Outpost speaks OpenID Connect, so Authentik, Authelia, Keycloak, Zitadel, Okta and Entra can all be the login for your instance.

It is configured from the environment, not from settings, because a client secret belongs with your deployment rather than in a database row. deploy/README.md has the walkthrough, and Single Sign-On covers what it changes for your members.

Manual install, if you would 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

Do this if you would rather not depend on the prebuilt ghcr.io image, or you are testing a change that has not shipped.

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

Migrations run themselves when the container starts. There is no separate migration step.

Backups

Two named Docker volumes hold everything that matters. outpost-pgdata for Postgres, meaning accounts, messages, channels and roles. outpost-minio for uploads.

Back up both. Treat them like any other stateful container.

How uploaded files are protected. They live in a private MinIO bucket and are served through the app, so a leaked or guessed file URL on its own gets nothing. Access is per instance rather than per file, though. Any signed-in member can fetch any object, and channel permissions are not rechecked at that point. Bear that in mind before treating a restricted channel as a private file store. Instances built before v0.2.17 had a public bucket, and their policy is cleared on startup now.

Server up? List it in the server directory so people can find it.