# Installing a Palaver server

| Component | Purpose | Path on `chat.example.org` |
|---|---|---|
| palaverd | accounts, channels, text | `/api/` |
| LiveKit | voice, screen sharing | `/livekit/` |
| web client | browser app | `/` |
| nginx | TLS (Let's Encrypt), reverse proxy | ports 80, 443 |
| PostgreSQL | data | – |

Updates: [UPDATE.md](UPDATE.md)

Placeholders in the commands:

| Placeholder | Replace with |
|---|---|
| `chat.example.org` | your domain name |
| `you@example.org` | e-mail address for Let's Encrypt |
| `0.3.0` | Palaver release |
| `https://<release host>/lasse-tech/palaver/releases/download` | release download location; until 1.0.0 on the project's internal Forgejo only |
| `<livekit-key>`, `<livekit-secret>` | output of step 6, "Generate the LiveKit API …" |
| `<cert-name>` | existing certificate only: its name |
| `vi` | editor of your choice |

## Contents

1. [Requirements](#1-requirements)
2. [Packages](#2-packages)
3. [User and directories](#3-user-and-directories)
4. [PostgreSQL](#4-postgresql)
5. [Download Palaver](#5-download-palaver)
6. [palaverd](#6-palaverd)
7. [LiveKit](#7-livekit)
8. [Logs](#8-logs)
9. [Web client](#9-web-client)
10. [nginx and certificate](#10-nginx-and-certificate)
11. [Start](#11-start)
12. [First admin](#12-first-admin)
13. [Check](#13-check)
- [Backup](#backup)
- [Uninstall](#uninstall)
- [Optional: existing certificate](#optional-existing-certificate)
- [Behind a home router](#behind-a-home-router)
- [Configuration reference](#configuration-reference)
- [Files](#files)
- [Troubleshooting](#troubleshooting)

## 1. Requirements

- OS: Debian 13 or a derivative based on Debian 13 or Ubuntu 24.04+ (e.g. Ubuntu, Linux Mint 22)
- architecture: amd64 (x86_64)
- public IPv4 address
- domain name with an A record to the server's public IPv4 address, e.g. `chat.example.org`
- e-mail address for Let's Encrypt
- shell access with `sudo`
- basic Linux administration skills

| Group | CPU | RAM | Disk |
|---|---|---|---|
| a few friends | 1 core | 2 GB | 20 GB |
| 20–30 people with screen sharing | 2 cores | 4 GB | 40 GB |

- bandwidth: ~1 Mbit/s per person in voice, +3–4 Mbit/s per viewer of a 1080p screen share
- attachments: up to 25 MiB each, stored in the database

Open ports to the server:

| Port | Protocol | For |
|---|---|---|
| 80 | TCP | Let's Encrypt, redirect to https |
| 443 | TCP | app, web client, voice signalling |
| 7881 | TCP | voice when UDP is blocked on the user's side |
| 3478 | UDP | STUN/TURN |
| 30000–30100 | UDP | TURN relay |
| 50000–60000 | UDP | voice and screen sharing |

Check the system.

```sh
. /etc/os-release; echo "$PRETTY_NAME | ${ID_LIKE:-$ID} | ${UBUNTU_CODENAME:-$VERSION_CODENAME} | $(uname -m)"
```

## 2. Packages

| Package | Requirement |
|---|---|
| PostgreSQL | 16 or newer, any source |
| nginx | any source |
| certbot | any source (apt, snap) |
| curl, ca-certificates, openssl | any source |

Install missing:

```sh
sudo apt-get update
sudo apt-get install postgresql nginx certbot curl ca-certificates openssl
```

## 3. User and directories

Create the system user `palaver`.

```sh
sudo useradd --system --home-dir /opt/palaver --no-create-home --shell /usr/sbin/nologin palaver
```

Create the directories.

```sh
sudo install -d -o root -g root -m 755 /opt/palaver /opt/palaver/bin /opt/palaver/web
sudo install -d -o palaver -g palaver -m 700 /opt/palaver/etc
```

## 4. PostgreSQL

Start PostgreSQL.

```sh
sudo systemctl enable --now postgresql
```

Create the database role and the database `palaver`.

```sh
sudo -u postgres createuser palaver
sudo -u postgres createdb -O palaver palaver
```

## 5. Download Palaver

Download the server, the web client and the checksums.

```sh
curl -fLO "https://<release host>/lasse-tech/palaver/releases/download/v0.3.0/palaverd-0.3.0-linux-amd64"
curl -fLO "https://<release host>/lasse-tech/palaver/releases/download/v0.3.0/palaver-web-0.3.0.tar.gz"
curl -fLO "https://<release host>/lasse-tech/palaver/releases/download/v0.3.0/SHA256SUMS"
```

Verify both files; each line must end in `OK`.

```sh
sha256sum -c --ignore-missing SHA256SUMS
```

## 6. palaverd

Install the binary.

```sh
sudo install -m 755 palaverd-0.3.0-linux-amd64 /opt/palaver/bin/palaverd
```

Generate the LiveKit API key (`<livekit-key>`).

```sh
echo "API$(openssl rand -hex 8)"
```

Generate the LiveKit API secret (`<livekit-secret>`).

```sh
openssl rand -base64 36 | tr -d '/+='
```

Open `/opt/palaver/etc/palaverd.env` in an editor.

```sh
sudo vi /opt/palaver/etc/palaverd.env
```

Content ([reference](#configuration-reference)):

```ini
PALAVER_DATABASE_URL=postgres:///palaver?host=/var/run/postgresql
PALAVER_LISTEN=127.0.0.1:8090
PALAVER_PUBLIC_URL=https://chat.example.org
PALAVER_LIVEKIT_URL=wss://chat.example.org/livekit
PALAVER_LIVEKIT_API_URL=http://127.0.0.1:7880
PALAVER_LIVEKIT_API_KEY=<livekit-key>
PALAVER_LIVEKIT_API_SECRET=<livekit-secret>
PALAVER_OPEN_REGISTRATION=false
```

Make it readable by `palaver` only.

```sh
sudo chown palaver:palaver /opt/palaver/etc/palaverd.env
sudo chmod 600 /opt/palaver/etc/palaverd.env
```

Open `/etc/systemd/system/palaverd.service` in an editor.

```sh
sudo vi /etc/systemd/system/palaverd.service
```

Content:

```ini
[Unit]
Description=Palaver app server
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
User=palaver
Group=palaver
EnvironmentFile=/opt/palaver/etc/palaverd.env
ExecStart=/opt/palaver/bin/palaverd serve
Restart=on-failure
RestartSec=2
LogNamespace=palaver

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
LockPersonality=yes
MemoryDenyWriteExecute=yes

[Install]
WantedBy=multi-user.target
```

## 7. LiveKit

Download LiveKit 1.13.7 from GitHub and verify it; the line must end in `OK`.

```sh
curl -fLO https://github.com/livekit/livekit/releases/download/v1.13.7/livekit_1.13.7_linux_amd64.tar.gz
echo "6634aeeb2fb1366b6723708ae4320b9d5408106a4c63457c5e845ae3979c90e2  livekit_1.13.7_linux_amd64.tar.gz" | sha256sum -c
```

Unpack and install the binary.

```sh
tar -xzf livekit_1.13.7_linux_amd64.tar.gz livekit-server
sudo install -m 755 livekit-server /opt/palaver/bin/livekit-server
```

Open `/opt/palaver/etc/livekit.yaml` in an editor.

```sh
sudo vi /opt/palaver/etc/livekit.yaml
```

Content:

```yaml
port: 7880
bind_addresses: ["127.0.0.1"]
rtc:
  tcp_port: 7881
  port_range_start: 50000
  port_range_end: 60000
  use_external_ip: false
  # node_ip: 203.0.113.10   # behind NAT only: the public IPv4
turn:
  enabled: true
  domain: chat.example.org
  udp_port: 3478
  relay_range_start: 30000
  relay_range_end: 30100
room:
  empty_timeout: 300
keys:
  <livekit-key>: <livekit-secret>
logging:
  level: info
```

Make it readable by `palaver` only.

```sh
sudo chown palaver:palaver /opt/palaver/etc/livekit.yaml
sudo chmod 600 /opt/palaver/etc/livekit.yaml
```

Open `/etc/systemd/system/livekit.service` in an editor.

```sh
sudo vi /etc/systemd/system/livekit.service
```

Content:

```ini
[Unit]
Description=LiveKit media server for Palaver
After=network-online.target
Wants=network-online.target

[Service]
User=palaver
Group=palaver
ExecStart=/opt/palaver/bin/livekit-server --config /opt/palaver/etc/livekit.yaml
Restart=on-failure
RestartSec=2
LogNamespace=palaver
LimitNOFILE=65536

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes

[Install]
WantedBy=multi-user.target
```

## 8. Logs

Open `/etc/systemd/journald@palaver.conf` in an editor.

```sh
sudo vi /etc/systemd/journald@palaver.conf
```

Content:

```ini
[Journal]
MaxRetentionSec=14day
```

Create the directory for nginx's logs of the site.

```sh
sudo install -d -o www-data -g adm -m 750 /var/log/nginx/palaver
```

Open `/etc/logrotate.d/palaver` in an editor.

```sh
sudo vi /etc/logrotate.d/palaver
```

Content:

```text
/var/log/nginx/palaver/*.log {
	daily
	rotate 13
	missingok
	notifempty
	compress
	delaycompress
	create 0640 www-data adm
	sharedscripts
	postrotate
		invoke-rc.d nginx rotate >/dev/null 2>&1
	endscript
}
```

## 9. Web client

Unpack the web client.

```sh
sudo tar -xzf palaver-web-0.3.0.tar.gz --no-same-owner -C /opt/palaver/web
sudo chmod -R u=rwX,go=rX /opt/palaver/web
```

Open `/etc/nginx/snippets/palaver-web.conf` in an editor.

```sh
sudo vi /etc/nginx/snippets/palaver-web.conf
```

Content:

```nginx
root /opt/palaver/web;
index index.html;

location / {
    try_files $uri /index.html;
    add_header Cache-Control "no-cache" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; worker-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob: mediastream:; font-src 'self'; connect-src 'self' wss://chat.example.org; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer" always;
    add_header Permissions-Policy "camera=(), geolocation=(), payment=()" always;
    add_header Strict-Transport-Security "max-age=31536000" always;
}

location /assets/ {
    try_files $uri =404;
    add_header Cache-Control "public, max-age=31536000, immutable" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; worker-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob: mediastream:; font-src 'self'; connect-src 'self' wss://chat.example.org; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=31536000" always;
}
```

## 10. nginx and certificate

Start nginx.

```sh
sudo systemctl enable --now nginx
```

Open `/etc/nginx/sites-available/palaver` in an editor.

```sh
sudo vi /etc/nginx/sites-available/palaver
```

Content:

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name chat.example.org;
    location /.well-known/acme-challenge/ { root /var/www/html; }
    location / { return 404; }
}
```

Enable the site, check and load it.

```sh
sudo ln -sf /etc/nginx/sites-available/palaver /etc/nginx/sites-enabled/palaver
sudo nginx -t && sudo systemctl reload nginx
```

Get the certificate.

```sh
sudo certbot certonly --webroot -w /var/www/html -d chat.example.org -m you@example.org \
    --agree-tos --no-eff-email --non-interactive \
    --deploy-hook "systemctl reload nginx"
```

Open `/etc/nginx/sites-available/palaver` in an editor.

```sh
sudo vi /etc/nginx/sites-available/palaver
```

Content:

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name chat.example.org;

    access_log /var/log/nginx/palaver/access.log;
    error_log /var/log/nginx/palaver/error.log;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name chat.example.org;

    ssl_certificate /etc/letsencrypt/live/chat.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/chat.example.org/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    access_log /var/log/nginx/palaver/access.log;
    error_log /var/log/nginx/palaver/error.log;

    client_max_body_size 64k;

    # server push, one WebSocket per client
    location = /api/events {
        proxy_pass http://127.0.0.1:8090;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 1h;
        proxy_send_timeout 1h;
    }

    # profile pictures, server icon: 5 MiB
    location ~ ^/api/(me/avatar|server/icon)$ {
        client_max_body_size 5m;
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # attachments: 25 MiB
    location ~ ^/api/channels/[0-9]+/attachments$ {
        client_max_body_size 25m;
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # LiveKit signalling; the trailing slash strips /livekit
    location /livekit/ {
        proxy_pass http://127.0.0.1:7880/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 1h;
        proxy_send_timeout 1h;
    }

    include snippets/palaver-web.conf;
}
```

Check and load it.

```sh
sudo nginx -t && sudo systemctl reload nginx
```

## 11. Start

Load the units, start LiveKit and palaverd.

```sh
sudo systemctl daemon-reload
sudo systemctl enable --now livekit palaverd
```

Check that palaverd answers; expected: JSON.

```sh
curl -fsS http://127.0.0.1:8090/api/registration
```

## 12. First admin

Create an invite code for the first admin; single use, valid 7 days.

```sh
sudo -u palaver /opt/palaver/bin/palaverd -env /opt/palaver/etc/palaverd.env invite -role Admin -valid 168h
```

- register: Palaver app or `https://chat.example.org` in a browser → server `https://chat.example.org` → **Register** → code
- first admin = server **owner**
- account registered without admin (code without `-role Admin`): give it the role

```sh
sudo -u palaver /opt/palaver/bin/palaverd -env /opt/palaver/etc/palaverd.env grant <username> Admin
```

- after the first sign-in: consent prompt for anonymous figures to Dogan (monitoring of Palaver servers);
  later under Settings › Server › Contribute
- invite others: **Administration mode** on (Settings › General › Administration, per device) → **Invite people**
  in the channel list

## 13. Check

Run doctor; expected: `ok` on every line.

```sh
sudo -u palaver /opt/palaver/bin/palaverd -env /opt/palaver/etc/palaverd.env doctor
```

- not checked: UDP ports; voice connects but stays silent: [ports](#1-requirements)

## Backup

- database: all data; messages end-to-end encrypted
- `/opt/palaver/etc`: configuration, LiveKit key

Dump the database.

```sh
sudo -u palaver pg_dump -Fc palaver > "palaver-$(date +%F).dump"
```

Save the configuration.

```sh
sudo tar -czf "palaver-etc-$(date +%F).tar.gz" -C /opt/palaver etc
```

### Restore

Prerequisite: new server, same domain, steps 1–11.

Restore the configuration.

```sh
sudo tar -xzf palaver-etc-<date>.tar.gz -C /opt/palaver
sudo chown -R palaver:palaver /opt/palaver/etc
```

Stop palaverd.

```sh
sudo systemctl stop palaverd
```

**Destructive:** delete the database of the new server (all its accounts and messages).

```sh
sudo -u postgres dropdb palaver
```

Create it again and load the dump.

```sh
sudo -u postgres createdb -O palaver palaver
sudo -u palaver pg_restore -d palaver palaver-<date>.dump
```

Start everything.

```sh
sudo systemctl restart livekit palaverd
```

## Uninstall

Stop and disable the services.

```sh
sudo systemctl disable --now palaverd livekit
```

Remove the units.

```sh
sudo rm /etc/systemd/system/palaverd.service /etc/systemd/system/livekit.service
sudo systemctl daemon-reload
```

Remove the nginx site and reload nginx.

```sh
sudo rm /etc/nginx/sites-enabled/palaver /etc/nginx/sites-available/palaver /etc/nginx/snippets/palaver-web.conf
sudo nginx -t && sudo systemctl reload nginx
```

Remove the journal and log rotation settings.

```sh
sudo rm /etc/systemd/journald@palaver.conf /etc/logrotate.d/palaver
```

Remove programs and web client.

```sh
sudo rm -r /opt/palaver/bin /opt/palaver/web
```

**Destructive:** delete the database and its role (all accounts, messages, attachments).

```sh
sudo -u postgres dropdb palaver
sudo -u postgres dropuser palaver
```

**Destructive:** delete the configuration (LiveKit key and secret) and the user `palaver`.

```sh
sudo rm -r /opt/palaver/etc
sudo rmdir /opt/palaver
sudo userdel palaver
```

**Destructive:** delete the certificate.

```sh
sudo certbot delete --cert-name chat.example.org
```

**Destructive:** delete the site's nginx logs (`/var/log/nginx/palaver`).

```sh
sudo rm -r /var/log/nginx/palaver
```

Close the ports in `ufw`; keep 80 and 443 if other sites use them.

```sh
sudo ufw delete allow 80,443,7881/tcp
sudo ufw delete allow 3478,30000:30100,50000:60000/udp
```

Kept: packages `postgresql`, `nginx`, `certbot`.

## Optional: existing certificate

For: certificate already in `/etc/letsencrypt/live/<name>/` (e.g. wildcard via DNS challenge) instead of step 10's.

List the certificate names.

```sh
sudo certbot certificates
```

[Step 10](#10-nginx-and-certificate): skip the port-80 site and `certbot certonly`.

After step 10's full site: open `/etc/nginx/sites-available/palaver` in an editor.

```sh
sudo vi /etc/nginx/sites-available/palaver
```

Change:

- `ssl_certificate`, `ssl_certificate_key`: `/etc/letsencrypt/live/chat.example.org/` → `/etc/letsencrypt/live/<cert-name>/`

## Behind a home router

- router: public IPv4 address, static or via dynamic DNS; no DS-Lite or other carrier-grade NAT
- forward all [ports](#1-requirements) to the server
- `/opt/palaver/etc/livekit.yaml`: replace the `# node_ip:` line with `  node_ip: <public IPv4>`; again on
  every address change

Restart LiveKit.

```sh
sudo systemctl restart livekit
```

- doctor's public-address checks may fail without NAT loopback; test from another device

## Configuration reference

- file: `/opt/palaver/etc/palaverd.env` (KEY=VALUE, systemd EnvironmentFile)
- after a change: `sudo systemctl restart palaverd`

| Variable | Default | Meaning |
|---|---|---|
| `PALAVER_DATABASE_URL` | – (required) | PostgreSQL connection string |
| `PALAVER_LISTEN` | `127.0.0.1:8080` | listen address; this guide uses `127.0.0.1:8090` |
| `PALAVER_PUBLIC_URL` | address the owner's client used | public address (`https://chat.example.org`); doctor checks it |
| `PALAVER_LIVEKIT_URL` | – | LiveKit URL for clients (`wss://chat.example.org/livekit`) |
| `PALAVER_LIVEKIT_API_URL` | `PALAVER_LIVEKIT_URL` as http(s) | LiveKit API for palaverd |
| `PALAVER_LIVEKIT_API_KEY` | – | key from `livekit.yaml` |
| `PALAVER_LIVEKIT_API_SECRET` | – | secret from `livekit.yaml` |
| `PALAVER_LIVEKIT_ROOM_PREFIX` | empty | room name prefix (`a-z`, `0-9`, `-`) |
| `PALAVER_OPEN_REGISTRATION` | `false` | `true`: sign-up without invite; an admin lets each account in, until then it sees the landing channel only |
| `PALAVER_TRUSTED_PROXIES` | localhost | addresses or CIDR prefixes of reverse proxies for `X-Forwarded-For`, comma-separated |
| `PALAVER_AUDIT_DAYS` | `14` | retention of the audit log (with IP addresses) in days |
| `PALAVER_WEB_URL` | – | web client address if not `/` (same host as `PALAVER_PUBLIC_URL`) |
| `PALAVER_DOGAN_URL` | `https://www.lasse-tech.de/dogan` | Dogan reporting; `off`: never report, the owner is not asked |
| `PALAVER_DOGAN_ENROLL` | – | one-time Dogan enrollment code; counts as the owner's consent |
| `PALAVER_CTL`, `PALAVER_UNIT`, `PALAVER_ENV_FILE` | this guide's command, `palaverd`, `/opt/palaver/etc/palaverd.env` | what doctor names in its hints |

- `palaverd -env FILE <command>`: loads the file; variables already in the environment take precedence
- `/opt/palaver/etc/livekit.yaml`: LiveKit settings (LiveKit documentation, `config-sample.yaml` in its repository);
  after a change: `sudo systemctl restart livekit`

## Files

| Path | Content |
|---|---|
| `/opt/palaver/bin/` | `palaverd`, `livekit-server` |
| `/opt/palaver/web/` | web client |
| `/opt/palaver/etc/` | `palaverd.env`, `livekit.yaml` (user `palaver` only) |
| `/etc/systemd/system/palaverd.service`, `livekit.service` | services |
| `/etc/systemd/journald@palaver.conf` | journal retention |
| `/etc/nginx/sites-available/palaver`, `/etc/nginx/snippets/palaver-web.conf` | nginx site |
| `/etc/logrotate.d/palaver`, `/var/log/nginx/palaver/` | nginx logs |

## Troubleshooting

Doctor: which part fails.

```sh
sudo -u palaver /opt/palaver/bin/palaverd -env /opt/palaver/etc/palaverd.env doctor
```

What palaverd and LiveKit say.

```sh
sudo journalctl --namespace=palaver -u palaverd -u livekit -n 100
```

What nginx says.

```sh
sudo tail -n 50 /var/log/nginx/palaver/error.log
```

| Symptom | Cause |
|---|---|
| certbot fails | port 80/TCP not reachable (hoster's firewall, router), or an AAAA record pointing elsewhere |
| voice connects, no sound | UDP ports ([requirements](#1-requirements)) |
| `could not change directory to "/home/…"` from `sudo -u postgres`/`sudo -u palaver` | harmless (no access to your home directory) |
| `palaverd failed err="… PALAVER_DATABASE_URL is not set"` | `-env /opt/palaver/etc/palaverd.env` missing |
| web client without voice in the browser | CSP: `wss://chat.example.org` missing in the snippet (doctor reports it) |

Help requests: attach the output of all three.
