Appearance
Quick Start
Run your own Dissent node on any VPS in under 10 minutes.
Prerequisites
- A VPS running Ubuntu or Debian (2 vCPU, 2 GB RAM recommended — ~$10–20/mo on most providers)
- A domain name with an A record pointing to your server's IP address
- The ports below open in your firewall
Ports
Chat needs two of these. Voice and video need all of them, and the UDP media port is the one people forget.
| Port | Protocol | Used for |
|---|---|---|
| 80 | TCP | Let's Encrypt certificate validation — required even though your site is HTTPS |
| 443 | TCP + UDP | The API, WebSocket and web client. UDP is HTTP/3 |
| 7882–7899 | UDP | WebRTC media (7882) and the built-in TURN relay (7883–7899). Voice and video do not work without this |
| 7881 | TCP | WebRTC over TCP — the fallback when UDP is blocked on the client's network |
Choosing a provider
Any VPS provider works. DigitalOcean, Hetzner, Vultr, and Linode are all solid choices. Hetzner offers the best price/performance in the EU.
Running at home instead of on a VPS has a real catch around that UDP port — see Home Server & Raspberry Pi.
Setup
You do not need a copy of the source. The node ships as a container image; these four files are the whole install.
1. Download the installer
bash
curl -fsSL https://docs.dissent.chat/install/fetch.sh -o fetch.sh
sh fetch.sh
cd dissent-nodeThat fetches docker-compose.yml, Caddyfile, setup.sh and env.example — and then stops, so you can read setup.sh before running it.
Why this is two commands and not one
You will find projects that tell you to pipe a downloaded script straight into a shell. We would rather you read what you are about to run on a machine you are about to put on the internet.
Pre-launch: the image is private
Dissent has not launched, and the node image is not public yet. Until it is, setup.sh cannot pull it unless the machine is signed in to the registry:
bash
echo <YOUR_TOKEN> | docker login ghcr.io -u <YOUR_GITHUB_USERNAME> --password-stdinThe token is a GitHub personal access token (classic) with the read:packages scope, created at github.com/settings/tokens. You need access to the project to have one.
Everything else on this page is exactly what it will be at launch — this login step is the only difference, and it disappears when the image goes public.
2. Run the setup script
bash
bash setup.shThe script will:
- Check Docker is installed, and offer to install it where it safely can
- Ask for your domain name
- Generate secrets automatically — including the at-rest encryption keys
- Pull the node image and start the stack
- Claim the node as your administrator account before anything is exposed
- Confirm the node is healthy by reading its
/healthresponse
Running on a NAS
Synology DSM, QNAP, Unraid and TrueNAS SCALE all work. Install Docker first from your NAS's own package manager — on Synology that is Package Center → Container Manager — then run setup.sh over SSH. The script detects DSM and will not try to install Docker itself, because the generic installer does not support it.
The first account is the administrator
setup.sh claims the node for you before it starts Caddy, and that ordering is deliberate. Requesting a certificate publishes your hostname to public Certificate Transparency logs within seconds, and those are routinely scanned. On an unclaimed node the first account to register becomes its administrator — so the window between "reachable" and "claimed" is one you do not want to exist.
Back up .env before you do anything else
The installer generates MESSAGE_ENC_KEY and PLUGIN_CONFIG_KEY, and your database is encrypted with them. A database backup without those keys restores to data nobody can read — including you. Copy .env somewhere off the server now, and see Backups & Restore.
3. Connect to your node
Once the script completes, your node is running at https://your-domain.com.
Open app.dissent.chat. On the login screen, above the sign-in card, is a Node: row showing which node you are about to sign in to. Click it to expand, then:
- under Self-hosted / Custom, type your node's URL (
https://your-domain.com) - click Use
The client probes the node and shows a green check when it answers. Your choice is remembered in that browser, so the next visit goes straight to your node.
Then register an account. Accounts belong to the node they were created on — an account on app.dissent.chat's official node is not an account on yours.
Switching later
Once signed in, the same choice lives in User Settings → Account → Node: the public nodes, a field for any other node URL, and a health check on each.
Switching signs you out, and the app says so before it acts. A session belongs to the node that issued it, so there is no way to be signed in to two nodes at once — and nothing on the node you leave is deleted.
Updating
When a new release is out, Node Admin → Overview shows a banner (your browser checks docs.dissent.chat/install/latest.json while you have Node Admin open — your node itself never contacts us). To update, on the machine running the node, in the folder you installed into:
bash
bash update.sh # newest release
bash update.sh 0.4.0 # or a specific oneIt finds your running node and the exact compose files it was started with (including behind-proxy mode), gets the new image, backs up the database and .env into ./backups/, restarts everything, waits until the node is healthy and serving the new web client — and puts the previous version back if any of that fails. Database migrations run on startup.
- A failed download changes nothing.
- It refuses to go to an older version: migrations only go forward.
- It never deletes backups — clear out
./backups/yourself. - Installed before
update.shexisted? Fetch it next tosetup.sh:curl -fsSLO https://docs.dissent.chat/install/update.sh
Troubleshooting
The installer waits two minutes for the node to answer, and it tells you which kind of failure it hit — the two look identical from the outside and have nothing in common underneath.
| What the installer printed | Cause | Fix |
|---|---|---|
Nothing answered at https://…/health | Your A record does not point at this server yet, ports 80/443 are blocked, or Caddy could not get a certificate | Check the A record resolves to this server's IP, open 80 and 443, then re-run bash setup.sh |
| Something answered, but it was not the API (followed by a snippet of HTML) | The request reached a web server but never reached the node | Check the /health route in your Caddyfile. Caddy falls through to index.html, so a misrouted path returns a page, not an error |
docker: command not found after install | Docker install requires a new shell session | Run source ~/.bashrc or log out and back in, then re-run bash setup.sh |
| Certificate error in browser | Caddy couldn't obtain a TLS cert | Ensure port 80 is open — Let's Encrypt validates over it, even for an HTTPS certificate |
Logs for anything else:
bash
docker compose -f docker-compose.prod.yml logs apiNext Steps
- Backups & Restore — do this before you invite anyone. Your node holds the only copy of your community's history
- Manual Setup — full control over every config option
- Storage — keep attachments off the server's disk
- Federation — connect your node to the wider Dissent network
- Home Server & Raspberry Pi — running without a VPS, and what that costs you
Your users can download everything the node holds about them from Settings → Account → Your data — worth knowing before somebody asks you for it.
Push notifications
Web push works out of the box. setup.sh generates VAPID keys for you; if you installed by hand, generate them with docker compose -f docker-compose.prod.yml run --rm gen-vapid and append the two lines to your .env before starting the API.
Push on the shipped Android app does not work against a self-hosted node, and this is not something you can configure. The published APK is built against this project's Firebase project, so those installs register for push under that project — your node has no credentials to send to them, and setting FIREBASE_SERVICE_ACCOUNT_JSON on your node cannot change which project a client you did not build is registered with.
Everything else on Android works normally against your node; only background push is affected, and messages arrive as soon as the app is opened. Building your own APK with your own google-services.json is the only way to change this.