Outpost is a self-hosted, single-community Discord alternative — text channels with roles/permissions, real voice and video (via LiveKit's WebRTC stack, not a toy), file uploads, invite-based joins, and a native desktop client, all under your own infrastructure. MIT licensed. This is the "one-click" Docker deployment, plus the one genuine gotcha that will silently break voice chat if you skip it.
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.
docker compose version should work)8080 (the app, configurable via APP_PORT), 7880
+ 7881 TCP (LiveKit signaling), 50000-60000 UDP (LiveKit media). The LiveKit ports
aren't configurable — LiveKit runs with Docker host networking rather than published ports, because mapping
10,000+ individual UDP ports through Docker's iptables-based port mapping is slow enough to effectively hang
docker compose up. Add 80 + 443 too if you're setting up HTTPS (see
below) — you should be.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.
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).
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
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
docker compose pull
docker compose up -d
Database migrations run automatically on container start — no separate migration step.
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 — see
Get Your Docker App Configs Actually Backed Up if you're not already
covering named volumes in your backup routine.