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.
Four containers.
The install script generates every secret and wires them together. A basic instance needs nothing hand-edited.
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.
docker compose version.8080 for the app, changeable with APP_PORT. 7880 and
7881 TCP for LiveKit signalling. 50000-60000 UDP for LiveKit media. Add
80 and 443 for HTTPS, which you want.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.
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.
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.
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.
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
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
docker compose pull
docker compose up -d
Migrations run themselves when the container starts. There is no separate migration step.
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.
Server up? List it in the server directory so people can find it.