Self-host Tusile

Run your own Tusile community on your own machine. You need Docker and Docker Compose. Nothing else: one script sets up the rest.

Offgrid needs nothing prepared. Cloud needs a domain and a server token you create in the app. The difference is one table down the page, and the script asks you which one you want.

Install it

  1. Unpack the zip.
  2. Run the setup script in the community-server folder.
    Windows: double-click setup.bat.
    Linux / macOS: chmod +x setup.sh then ./setup.sh
  3. At Choice [1]: pick 1) Cloud or 2) Offgrid, then answer the prompts. The script does the rest: generates LiveKit keys, writes .env and the Caddyfile, configures HTTPS and TURN/TLS, and starts the stack.
  4. Open the Tusile app and connect to your server.

The script is safe to re-run. If the stack does not come up it prints the failing container's logs and you run it again. It does the same steps you would do by hand, and you are welcome to read it first.

Cloud or Offgrid

This is the only choice that changes what you need. Everything after it is the same either way.

CloudOffgrid
Accountstusile.comusernames and passwords that exist only on your server
Contacts tusile.comyesnever, from either the server or the app
You needa server token created in the app, and a public hostnameany address the app can reach, a LAN IP such as 192.168.1.50 included
Listed in the app's server listyesno

No hostname yet? Duck DNS, No-IP, and FreeDNS all hand out a free subdomain you point at your server's IP. Offgrid does not need one: the script sets up a local certificate for whatever address you give it, and the app asks you to confirm that certificate the first time you connect.

Can I move a Cloud server to Offgrid?

No. Offgrid is for new installs. Existing members are identified by their Tusile account, and on an Offgrid server there is nothing local to map them onto. Self-hosting either way means your community's messages live on hardware you control — see where Tusile stands on EU Chat Control.

First run takes a few minutes

Text chat works as soon as setup finishes. Voice can lag around six minutes behind on a first run with a public domain: LiveKit will not start without its TURN certificate, Caddy has to obtain that from Let's Encrypt first, and the livekit-cert-sync sidecar copies it across on a five-minute cycle. LiveKit restart-loops until then. That is expected, and it fixes itself.

docker compose logs -f livekit

Connect from the app

Cloud: your server appears in the app's server list. Sign in with your Tusile account and open it.

Offgrid: the server starts with no accounts at all, so setup prints a one-time owner claim token when it finishes:

OWNER CLAIM TOKEN   4f8c2a91-ee03-7b1d-a52f-91c4
Valid 24h, single use.
  1. Turn the Offgrid toggle on, on the sign-in screen or at the top of the server rail.
  2. Enter your server's address and press Connect. Use the same address you gave the script: a certificate issued for 127.0.0.1 is not valid for localhost, and the connection fails if the two disagree.
  3. Choose Create account, pick a username and password, and paste the claim token.

That first account becomes the owner and gets the Admin role. The token is then spent, and everyone else joins with an invite you create from the app. New servers are invite-only by default; you can switch that to open or closed in Server settings.

Lost the claim token?

It stays on the data volume until it is used, so you can read it again at any time. No output means the server has already been claimed.

docker compose exec -T community-server cat /app/data/owner_claim_token

If it expired, or the server was claimed by the wrong account, clear the owner and restart. A fresh token is issued on the next boot. This needs shell access to the machine, which is the point.

docker compose exec postgres psql -U tusile_community -d tusile_community \
  -c "UPDATE server_settings SET owner_user_id = NULL"
docker compose restart community-server

Check it works

docker compose ps                 # every container up or healthy
curl https://your-domain/health   # -> ok
curl https://your-domain/info     # Offgrid: "offgrid": true, claim_available

Then join from the Tusile client and try text and voice in one channel. If something is wrong, Troubleshooting lists the usual causes by symptom.

To prove an Offgrid server really is standalone, block outbound traffic to api.tusile.com and confirm nothing breaks. docker compose logs community-server | grep -i tusile.com should stay empty.

Ports to open

The setup script prints this list when it finishes. Open them on your router or firewall. A LAN-only Offgrid server needs nothing forwarded.

PortWhy
80/tcp, 443/tcpCaddy: the community API, and the Let's Encrypt challenge. Port 80 is not optional on Cloud.
7880/tcpLiveKit WebSocket, through Caddy.
7881/tcpLiveKit ICE/TCP fallback.
50000-50060/udpRTP media. Required for voice and video.
3478/udpTURN/UDP and STUN. Helps clients behind strict firewalls.
5349/tcpTURN/TLS. Setup enables it and livekit-cert-sync feeds it the cert, so just open the port. Without it, users on UDP-blocked or symmetric-NAT networks get one-way audio.

Port 9090 is also exposed for direct access to the community API. With Caddy in front, 80 and 443 are the main entry.

No port forwarding? Use a tunnel

If you cannot forward ports (your ISP uses CGNAT, or the router is not yours), or you would rather members never see your home IP, put a small VPS in front. A WireGuard tunnel links the VPS to the machine running the stack, the VPS forwards the ports above through it, and your home network opens nothing. It works for Cloud and Offgrid alike.

  1. Bring up WireGuard between the two machines. The examples below use 10.8.0.1 for the VPS and 10.8.0.2 for your machine, on the tunnel.
  2. Point your hostname at the VPS public IP, and give setup that hostname.
  3. On the VPS, allow the ports from the table above in its firewall and forward them into the tunnel (eth0 is the VPS's public interface):
    sysctl -w net.ipv4.ip_forward=1
    iptables -t nat -A PREROUTING -i eth0 -p tcp -m multiport --dports 80,443,7880,7881,5349 -j DNAT --to-destination 10.8.0.2
    iptables -t nat -A PREROUTING -i eth0 -p udp -m multiport --dports 3478,50000:50060 -j DNAT --to-destination 10.8.0.2
    Put these in the VPS's WireGuard config as PostUp lines so they survive a reboot.
  4. Send the replies back the same way. The simplest route is iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE on the VPS, but then every member reaches your server from 10.8.0.1 and they all share its per-IP rate limits. That is fine for a group of friends. For a bigger community, skip the masquerade and set AllowedIPs = 0.0.0.0/0 for the VPS peer on your machine instead, so its traffic leaves through the tunnel and members keep their own addresses.
  5. Tell LiveKit to advertise the VPS. It finds its public IP by asking from your machine, which returns your home address, not the VPS. In livekit.prod.yaml set node_ip: <VPS public IP> and use_external_ip: false, then run docker compose restart livekit. If you skip this, text works and voice connects, but nobody hears anything.

Tunnel services that forward only HTTP(S) cannot carry the UDP media ports, so text chat works through them but voice does not.

Install by hand, without the script

The bundle is a normal Compose stack: the community server, Postgres, Caddy for HTTPS, LiveKit for voice and video, and a small cert-sync sidecar. The setup script only writes config files, so you can write them yourself.

  1. Unpack the archive: unzip community-server.zip and cd community-server.
  2. Create your .env: cp .env.example .env. The bundle deliberately ships no .env — an existing one is how the tooling recognises a previous install, and a pre-made file made every fresh install look like an upgrade.
  3. Set these in .env:
    • COMMUNITY_PUBLIC_URL — your server's public URL, no path. Must match the domain in the Caddyfile. Required in both modes; it drives voice and attachment URLs.
    • SERVER_TOKENCloud only. The token from the Tusile app that registers your community.
    • OFFGRID_MODE=trueOffgrid only. Leave SERVER_TOKEN empty; the server mints its own signing key on first boot and never calls tusile.com.
    • LIVEKIT_API_KEY and LIVEKIT_API_SECRET — generate a pair and put it in both .env and the keys: block of livekit.prod.yaml:
      docker run --rm livekit/livekit-server generate-keys
      There is no default. The stack will not start without them. Bundles from before 2 September 2026 fell back to a demo pair that was published in our own documentation, which let anyone who knew you ran Tusile join your voice channels; if you are upgrading from one of those and Compose now stops with required variable LIVEKIT_API_KEY is missing a value, that is this change, and generating a pair is the whole fix.
    Everything else has a working default. See the configuration reference for the full list.
  4. Edit Caddyfile: replace community.example.com with your domain. Caddy obtains TLS certificates automatically.
  5. Enable TURN/TLS (recommended): in livekit.prod.yaml uncomment tls_port, domain, cert_file, and key_file, setting domain to your hostname. The livekit-cert-sync container then supplies the cert from Caddy, using the host of COMMUNITY_PUBLIC_URL (set TURN_DOMAIN only to override it).
  6. Start the stack:
    docker compose up -d

Offgrid without a domain: Let's Encrypt will not issue for an IP address, so Caddy has to use its own CA. Do not use a bare tls internal — the app pins the certificate fingerprint and the default internal certificate lives only 12 hours, so the connection breaks after the first renewal. The exact stanzas, and why the web client cannot use a self-signed server at all, are in App says the certificate changed.

Each Caddy site block gets its own certificate, matched by name. A block for 127.0.0.1 will not answer for localhost: it aborts the handshake rather than presenting anything. Connect using exactly the address you configured, or list both names on the block.

Unattended install (cloud-init, CI, fleets)

Every answer can also be given as a flag, so nothing prompts. Supply the mode, the hostname, and for Cloud the token:

./setup.sh --offgrid --hostname chat.example.com --yes
./setup.sh --cloud --token YOUR_TOKEN --hostname chat.example.com --yes
./setup.sh --help

Windows uses the same names as PowerShell parameters, and setup.bat forwards whatever you pass it (skipping its press-a-key pause when given arguments):

.\setup.ps1 -Offgrid -HostName chat.example.com -Yes
.\setup.ps1 -Cloud -Token YOUR_TOKEN -HostName chat.example.com -Yes

Check the exit code rather than parsing the output:

CodeMeaning
0the stack is up and serving
1bad arguments, or a host a Cloud install cannot work on
2the stack did not come up. The failing logs are printed, and re-running is safe.

For a full cloud walkthrough — instance sizing, security-group rules, Docker via cloud-init, teardown — see Host on AWS EC2.

Back up your server

The stack keeps DATA_DIR on the community-data volume. Back it up together with the database — see backup and restore.

This matters far more on Offgrid. That volume holds community_key, the signing key the server mints on first boot. Your server's identity is derived from it, so losing it invalidates every session, changes the server ID, renames every voice room, and breaks custom emoji. On a Cloud server the same loss only costs an install ID. Restoring community_key plus the database restores the server completely.

Do not change COMPOSE_PROJECT_NAME on a running deployment. It prefixes every volume name, so changing it silently orphans your database and your uploads.