Self-Host a Private Stoat Chat Server
Secure a fresh Droplet, install Stoat’s official Docker Compose stack, and prove the web client works before you invite anyone.
Create the Droplet and firewall
Create a fresh Ubuntu Droplet with at least 2 vCPUs and 2 GB RAM. Add an SSH key during creation. Then create a Cloud Firewall, attach it to this Droplet, and point a hostname such as chat.example.com at its public IPv4 address. This is a Docker Compose deployment, not an App Platform app or one-container demo.
Before you create the server, confirm the current Droplet plan price and any domain cost you will carry. The server stays provisioned while the chat is idle, and optional calls add public ports and more capacity to operate. Start with text chat; add calls only after the private web path is proven.
- TCP 22 from your own public IP or VPN only.
- TCP 80 and 443 from everywhere for the web client and HTTPS certificates.
- TCP 7881 and UDP 50000 through 50100 from everywhere only if you want LiveKit calls.
- Leave MongoDB, Valkey, RabbitMQ, MinIO, and Docker without public host ports.
Attach the firewall before you connect
In the Cloud Firewall form, create the inbound rules below and choose this server under Apply to Droplets. Keep DigitalOcean's default outbound allow-all rules. The two call rules are optional. Do not add them if your community will not use calls.
Connect, harden, then install
Use the Droplet's public IPv4 address from the DigitalOcean dashboard. The hardening script updates Ubuntu and turns off SSH password login only when it finds a root SSH key. Keep this terminal open, then confirm a second SSH login works. The preflight script changes nothing. The install script requires --apply, refuses to overwrite /opt/stoat, and opens Stoat's configuration generator.
Before continuing: This updates Ubuntu and disables SSH password login after confirming a root SSH key exists. Keep the first SSH session open and test a second login.
- Replace every highlighted value before running this command.
ssh root@YOUR_DROPLET_IP git clone https://github.com/TylorMayfield/stoat-droplet-runbook.gitcd stoat-droplet-runbooksudo ./scripts/harden-host.sh --apply
Before continuing: This installs Docker and writes Stoat secrets on a new server. Confirm the Cloud Firewall and DNS before you run the final install step.
cd ~/stoat-droplet-runbook- Replace every highlighted value before running this command.
sudo ./scripts/preflight.sh chat.example.com - Replace every highlighted value before running this command.
sudo ./scripts/install-official-stoat.sh --apply chat.example.com
Require invitations before the first start
Open /opt/stoat/Revolt.toml in the server editor. In its existing api.registration section, set invite_only = true; do not duplicate the section. Save before starting services. HTTPS protects the connection; this setting restricts account creation.
[api.registration]
invite_only = trueStart once, then check HTTPS
The configuration generator creates secrets.env. Back it up before you start the service. Start in the foreground, read errors if any appear, then stop with Ctrl+C and run the detached command. The verification script checks the Compose configuration, running services, and public HTTPS without printing secrets. It does not prove the web app works yet.
If the verification cannot reach HTTPS, check the hostname’s DNS record and that the Cloud Firewall is attached before opening more ports. If a container is unhealthy, read only the relevant Compose service output and redact it before sharing. Do not solve either symptom by publishing a backing service or copying secrets into a ticket.
Before continuing: The foreground command starts the full Stoat service stack. Read its output before you stop it and start the detached service.
cd /opt/stoatsudo docker compose upsudo docker compose up -d- Replace every highlighted value before running this command.
sudo ~/stoat-droplet-runbook/scripts/verify-stoat.sh chat.example.com
Test rejection, then invited registration
In a fresh browser profile, open https://chat.example.com and try registering without an invitation. It must fail. Then create an invitation with the server command below. Save the printed code in a password manager, not a ticket or screenshot. Use it to create your test account, then send one message and upload a disposable file.
Test calls from a second network if enabled. Use the web client or its PWA; self-hosted support varies among official clients. Do not invite others if registration without a code succeeds or a data service has a public port.
cd /opt/stoat
sudo docker compose exec database mongosh revolt --quiet --eval 'const invite = require("crypto").randomBytes(24).toString("hex"); db.account_invites.insertOne({_id: invite}); print(invite);'Make an offline backup before inviting others
For the unmodified official Compose layout, persistent files live under /opt/stoat/data. Save the script below as /root/backup-stoat.sh. Run sudo bash /root/backup-stoat.sh during a maintenance window. It stops the currently running services, archives the entire installation including hidden configuration and data, and starts only those services again. Changed mount paths or external databases need their own backup.
The root-only archive contains passwords and private messages. Copy it through your approved encrypted backup process to a separate machine, then verify the archive listing includes secrets.env, Revolt.toml and data/. On an isolated spare server, restore the directory and original Compose configuration with inbound access restricted; verify the test account, message and file. Do not use the production hostname or let the spare send notifications.
#!/usr/bin/env bash
set -euo pipefail
umask 077
cd /opt/stoat
mapfile -t running < <(docker compose ps --services --status running)
resume() {
if (( ${#running[@]} )); then docker compose start "${running[@]}"; fi
}
trap resume EXIT
if (( ${#running[@]} )); then docker compose stop "${running[@]}"; fi
install -d -m 700 /var/backups/stoat
archive="/var/backups/stoat/stoat-$(date -u +%Y%m%dT%H%M%SZ).tar.gz"
tar -C /opt -czf "$archive" stoat
tar -tzf "$archive" > /dev/null
printf "Backup: %s\n" "$archive"Check your result
- Expected result
- Uninvited registration fails; an invited account can send a test message and upload a file.
- Stop if
- Stop if SSH is open to the internet, a data service has a host port, DNS does not point to the Droplet, a certificate fails, a secret appears in output, or a LiveKit port is open without a deliberate call-testing plan.
- Next step
- Back up configuration and persistent data, test a restore away from production, then record the repository commit and image versions before inviting users.