// palaver · mehrere server einrichten

Weitere Palaver-Server auf einem Host

Eigen je weiterem Server:

Bestandteil Wert
Name <name>, z. B. guild
Domain z. B. guild.palaver.social
Systembenutzer, Datenbankrolle palaver-<name>
Datenbank palaver_<name> (- wird _)
Konfiguration /opt/palaver/instances/<name>/palaverd.env
systemd-Unit palaverd@<name>
Port ab 8091, siehe Port
LiveKit-Raumpräfix <name>- (PALAVER_LIVEKIT_ROOM_PREFIX)
LiveKit-Schlüssel eigener Schlüssel in /opt/palaver/etc/livekit.yaml
nginx Site palaver-<name>, Snippet palaver-web-<name>.conf
nginx-Logs /var/log/nginx/palaver/<name>.access.log, <name>.error.log

1. Voraussetzungen

  • Hauptserver nach Palavers INSTALL.md (palaverd.service, Benutzer und Datenbank palaver, /opt/palaver/etc, LiveKit, Snippet palaver-web.conf), Port 8090
  • Shell-Zugang mit sudo; bash (${VAR//-/_})
  • keine weiteren Ports: 443 und die LiveKit-Ports des Hauptservers

Prüfe, ob der Hauptserver da ist.

sudo ls /opt/palaver/etc/palaverd.env /opt/palaver/etc/livekit.yaml

2. Variablen

Setze die Variablen; gelten nur in dieser Shell.

NAME=guild
DOMAIN=guild.palaver.social
EMAIL=you@example.org
CERT=$DOMAIN
  • NAME: a-z, 0-9, - (nicht als erstes oder letztes Zeichen), höchstens 24 Zeichen
  • reserviert: palaver, www, account, konto, support, api, admin, dogan, livekit, mail, status
  • DOMAIN: Hostname, kleingeschrieben
  • EMAIL: Let's Encrypt; nur eigenes Zertifikat
  • CERT: Zertifikatsname in /etc/letsencrypt/live/; eigenes Zertifikat: $DOMAIN; unter dem Wildcard-Zertifikat: dessen Name (z. B. palaver.social)

Setze die abgeleiteten Namen.

SRV_USER=palaver-$NAME
DB=palaver_${NAME//-/_}
CONF=/opt/palaver/instances/$NAME
UNIT=palaverd@$NAME

Prüfe den Namen; erwartet: ok.

[[ $NAME =~ ^[a-z0-9]([a-z0-9-]{0,22}[a-z0-9])?$ ]] && ! [[ " palaver www account konto support api admin dogan livekit mail status " == *" $NAME "* ]] && echo ok

Prüfe, ob der Name frei ist; erwartet: beide Befehle schlagen fehl.

ls -d "$CONF"
id "$SRV_USER"

3. DNS

  • Wildcard-Eintrag (*.palaver.social): nichts zu tun
  • sonst A-Eintrag: $DOMAIN → öffentliche IPv4-Adresse des Hosts

Prüfe den Eintrag.

getent ahostsv4 "$DOMAIN"

Zeige die Adressen des Hosts; erwartet: der A-Eintrag ist darunter.

ip -o addr show scope global

4. Port

  • ein localhost-Port je Server; Hauptserver: 8090
  • neuer Server: höchster belegter Port + 1

Zeige die belegten Ports.

sudo sh -c 'grep -H "^PALAVER_LISTEN=" /opt/palaver/etc/palaverd.env /opt/palaver/instances/*/palaverd.env 2>/dev/null'

Setze den nächsten Port.

PORT=$(( $(sudo sh -c 'cat /opt/palaver/etc/palaverd.env /opt/palaver/instances/*/palaverd.env 2>/dev/null' | sed -n 's/^PALAVER_LISTEN=.*://p' | sort -n | tail -n 1) + 1 )); echo "$PORT"

5. Unit-Vorlage

Einmal je Host; überspringen, wenn /etc/systemd/system/palaverd@.service existiert.

ls /etc/systemd/system/palaverd@.service

Schreibe die Unit-Vorlage; %i ist der Name des Servers.

sudo tee /etc/systemd/system/palaverd@.service >/dev/null <<'EOF'
[Unit]
Description=Palaver app server %i
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
User=palaver-%i
Group=palaver-%i
EnvironmentFile=/opt/palaver/instances/%i/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
EOF
sudo systemctl daemon-reload

6. Benutzer und Verzeichnis

Lege das Verzeichnis für alle weiteren Server an; einmal je Host, unschädlich, wenn es existiert.

sudo install -d -o root -g root -m 755 /opt/palaver/instances

Lege den Systembenutzer an.

sudo useradd --system --home-dir "$CONF" --no-create-home --shell /usr/sbin/nologin "$SRV_USER"

Lege das Konfigurationsverzeichnis an, nur für den Benutzer des Servers lesbar.

sudo install -d -o "$SRV_USER" -g "$SRV_USER" -m 700 "$CONF"

7. PostgreSQL

Lege die Datenbankrolle und die Datenbank an; Unix-Socket, Peer-Authentifizierung, kein Passwort.

sudo -u postgres createuser "$SRV_USER"
sudo -u postgres createdb -O "$SRV_USER" "$DB"

8. LiveKit-Schlüssel

Erzeuge LiveKit-API-Schlüssel und -Geheimnis des Servers; gebraucht in den Schritten 8 und 9.

KEY="API$(openssl rand -hex 8)"
SECRET=$(openssl rand -base64 36 | tr -d '/+=')

Trage den Schlüssel unter keys: in LiveKits Konfiguration ein.

sudo sed -i "/^keys:\$/a\\  $KEY: $SECRET" /opt/palaver/etc/livekit.yaml

Prüfe die Schlüssel; erwartet: eine Zeile je Server.

sudo sed -n '/^keys:/,/^[a-z]/p' /opt/palaver/etc/livekit.yaml

Starte LiveKit neu (liest Schlüssel nur beim Start); Gespräche auf allen Servern brechen kurz ab und verbinden sich neu.

sudo systemctl restart livekit
  • LiveKit-Schlüssel sind nicht auf Räume begrenzt: die Konfiguration eines Servers gibt Zugriff auf die Gespräche aller Server

9. palaverd

Lege die Konfigurationsdatei an, nur für den Benutzer des Servers lesbar.

sudo install -o "$SRV_USER" -g "$SRV_USER" -m 600 /dev/null "$CONF/palaverd.env"

Schreibe die Konfiguration; letzte drei Zeilen: die Hinweise von doctor nennen diesen Server.

sudo tee "$CONF/palaverd.env" >/dev/null <<EOF
PALAVER_DATABASE_URL=postgres:///$DB?host=/var/run/postgresql
PALAVER_LISTEN=127.0.0.1:$PORT
PALAVER_PUBLIC_URL=https://$DOMAIN
PALAVER_LIVEKIT_URL=wss://$DOMAIN/livekit
PALAVER_LIVEKIT_API_URL=http://127.0.0.1:7880
PALAVER_LIVEKIT_API_KEY=$KEY
PALAVER_LIVEKIT_API_SECRET=$SECRET
PALAVER_LIVEKIT_ROOM_PREFIX=$NAME-
PALAVER_OPEN_REGISTRATION=false
PALAVER_CTL="sudo -u $SRV_USER /opt/palaver/bin/palaverd -env $CONF/palaverd.env"
PALAVER_UNIT=$UNIT
PALAVER_ENV_FILE=$CONF/palaverd.env
EOF

10. Zertifikat

Unter einem Wildcard-Zertifikat

CERT = Name des Wildcard-Zertifikats. Prüfe, ob es $DOMAIN abdeckt; erwartet: DNS:*.<übergeordnete Domain>.

sudo openssl x509 -noout -ext subjectAltName -in "/etc/letsencrypt/live/$CERT/fullchain.pem"

Weiter: Schritt 11.

Eigenes Zertifikat

CERT = $DOMAIN. Schreibe eine vorläufige Site auf Port 80 für die Let's-Encrypt-Prüfung.

sudo tee "/etc/nginx/sites-available/palaver-$NAME" >/dev/null <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name $DOMAIN;
    location /.well-known/acme-challenge/ { root /var/www/html; }
    location / { return 404; }
}
EOF
sudo ln -sf "/etc/nginx/sites-available/palaver-$NAME" "/etc/nginx/sites-enabled/palaver-$NAME"
sudo nginx -t && sudo systemctl reload nginx

Hole das Zertifikat; automatische Erneuerung, lädt nginx neu.

sudo certbot certonly --webroot -w /var/www/html -d "$DOMAIN" -m "$EMAIL" \
    --agree-tos --no-eff-email --non-interactive \
    --deploy-hook "systemctl reload nginx"

11. nginx

Schreibe das Web-Snippet des Servers (wie Palavers INSTALL.md, Schritt 9); sed setzt @DOMAIN@ ein.

sudo tee "/etc/nginx/snippets/palaver-web-$NAME.conf" >/dev/null <<'EOF'
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://@DOMAIN@; 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://@DOMAIN@; 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;
}
EOF
sudo sed -i "s/@DOMAIN@/$DOMAIN/g" "/etc/nginx/snippets/palaver-web-$NAME.conf"

Schreibe die vollständige Site; sed setzt @DOMAIN@, @CERT@, @PORT@, @NAME@ ein.

sudo tee "/etc/nginx/sites-available/palaver-$NAME" >/dev/null <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name @DOMAIN@;

    access_log /var/log/nginx/palaver/@NAME@.access.log;
    error_log /var/log/nginx/palaver/@NAME@.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 @DOMAIN@;

    ssl_certificate /etc/letsencrypt/live/@CERT@/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/@CERT@/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

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

    client_max_body_size 64k;

    # server push, one WebSocket per client
    location = /api/events {
        proxy_pass http://127.0.0.1:@PORT@;
        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:@PORT@;
        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:@PORT@;
        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:@PORT@;
        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, shared; 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-@NAME@.conf;
}
EOF
sudo sed -i -e "s/@DOMAIN@/$DOMAIN/g" -e "s/@CERT@/$CERT/g" -e "s/@PORT@/$PORT/g" -e "s/@NAME@/$NAME/g" "/etc/nginx/sites-available/palaver-$NAME"

Aktiviere, prüfe und lade sie.

sudo ln -sf "/etc/nginx/sites-available/palaver-$NAME" "/etc/nginx/sites-enabled/palaver-$NAME"
sudo nginx -t && sudo systemctl reload nginx

12. Starten

Starte den Server; der erste Start legt die Datenbanktabellen an.

sudo systemctl enable --now "$UNIT"

Prüfe, ob er antwortet; erwartet: JSON.

curl -fsS "http://127.0.0.1:$PORT/api/registration"

13. Erster Admin

Erzeuge einen Einladungscode für den ersten Admin; einmal nutzbar, 7 Tage gültig.

sudo -u "$SRV_USER" /opt/palaver/bin/palaverd -env "$CONF/palaverd.env" invite -role Admin -valid 168h
  • registrieren: Palaver-App → Server https://$DOMAIN → Registrieren → Code
  • erster Admin = Eigentümer des Servers

14. Prüfen

Führe doctor aus; erwartet: ok in jeder Zeile.

sudo -u "$SRV_USER" /opt/palaver/bin/palaverd -env "$CONF/palaverd.env" doctor

Prüfe, ob der Hauptserver weiter antwortet.

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

Server auflisten

Namen der weiteren Server.

ls /opt/palaver/instances/

Domain und Port jedes Servers.

sudo sh -c 'grep -H -E "^PALAVER_(PUBLIC_URL|LISTEN)=" /opt/palaver/etc/palaverd.env /opt/palaver/instances/*/palaverd.env'

Zustand jedes Servers.

systemctl list-units --all 'palaverd*'

Sicherung

  • Datenbank: alle Daten des Servers
  • $CONF: Konfiguration, LiveKit-Schlüssel
  • Voraussetzung: NAME und abgeleitete Namen (Schritt 2)

Sichere die Datenbank.

sudo -u "$SRV_USER" pg_dump -Fc "$DB" > "palaver-$NAME-$(date +%F).dump"

Sichere die Konfiguration.

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

Wiederherstellen

Voraussetzung: Server mit demselben Namen und derselben Domain, Schritte 1–12.

Halte ihn an.

sudo systemctl stop "$UNIT"

Destruktiv: Lösche die Datenbank des neuen Servers ($DB, alle seine Konten und Nachrichten).

sudo -u postgres dropdb "$DB"

Lege sie neu an und lade die Sicherung.

sudo -u postgres createdb -O "$SRV_USER" "$DB"
sudo -u "$SRV_USER" pg_restore -d "$DB" palaver-<name>-<date>.dump

Starte ihn.

sudo systemctl start "$UNIT"

Server entfernen

Voraussetzung: NAME und abgeleitete Namen (Schritt 2).

Lies Domain und LiveKit-Schlüssel aus.

DOMAIN=$(sudo sed -n 's#^PALAVER_PUBLIC_URL=https://##p' "$CONF/palaverd.env"); echo "$DOMAIN"
KEY=$(sudo sed -n 's/^PALAVER_LIVEKIT_API_KEY=//p' "$CONF/palaverd.env"); echo "$KEY"

Zeige das Zertifikat der Site; eigenes Zertifikat: Name $DOMAIN.

grep ssl_certificate "/etc/nginx/sites-available/palaver-$NAME"

Vorher sichern: Sicherung.

Halte den Dienst an und deaktiviere ihn.

sudo systemctl disable --now "$UNIT"

Entferne nginx-Site und -Snippet und lade nginx neu.

sudo rm "/etc/nginx/sites-enabled/palaver-$NAME" "/etc/nginx/sites-available/palaver-$NAME" "/etc/nginx/snippets/palaver-web-$NAME.conf"
sudo nginx -t && sudo systemctl reload nginx

Destruktiv: Lösche die Datenbank $DB (alle Konten, Nachrichten, Anhänge) und ihre Rolle $SRV_USER; nicht rückgängig zu machen.

sudo -u postgres dropdb "$DB"
sudo -u postgres dropuser "$SRV_USER"

Destruktiv: Lösche den LiveKit-Schlüssel des Servers (die Zeile $KEY: … in /opt/palaver/etc/livekit.yaml).

sudo sed -i "/^  $KEY: /d" /opt/palaver/etc/livekit.yaml

Starte LiveKit neu; Gespräche auf den anderen Servern brechen kurz ab.

sudo systemctl restart livekit

Destruktiv: Lösche die Konfiguration $CONF und den Benutzer $SRV_USER.

sudo rm -r "$CONF"
sudo userdel "$SRV_USER"

Destruktiv: Lösche das Zertifikat; nur ein eigenes ($DOMAIN), nie das Wildcard-Zertifikat.

sudo certbot delete --cert-name "$DOMAIN"

Destruktiv: Lösche die nginx-Logs des Servers (/var/log/nginx/palaver/$NAME.*).

sudo sh -c "rm /var/log/nginx/palaver/$NAME.access.log* /var/log/nginx/palaver/$NAME.error.log*"

Lösche den DNS-Eintrag von $DOMAIN; nur ein eigener Eintrag.

Nach dem letzten weiteren Server

Entferne die Unit-Vorlage.

sudo rm /etc/systemd/system/palaverd@.service
sudo systemctl daemon-reload

Entferne das leere Verzeichnis.

sudo rmdir /opt/palaver/instances

Optional: unbekannte Namen zum Hauptserver

  • für: Wildcard-DNS-Eintrag und -Zertifikat für die Domain des Hauptservers
  • unbekannte Namen (Tippfehler): Umleitung zum Hauptserver statt zu nginx' erster Site

Setze die Domain des Hauptservers und den Namen seines Zertifikats.

MAIN=palaver.social
MAIN_CERT=palaver.social

Prüfe, ob das Zertifikat *.$MAIN abdeckt.

sudo openssl x509 -noout -ext subjectAltName -in "/etc/letsencrypt/live/$MAIN_CERT/fullchain.pem"

Prüfe, ob die Platzhalter-Site der Distribution default_server beansprucht.

grep default_server /etc/nginx/sites-enabled/default

Falls ja: deaktiviere sie; bleibt in sites-available.

sudo rm /etc/nginx/sites-enabled/default

Schreibe die Catch-all-Site; sed setzt @MAIN@, @MAIN_CERT@ ein.

sudo tee /etc/nginx/sites-available/palaver-catch-all >/dev/null <<'EOF'
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    location /.well-known/acme-challenge/ { root /var/www/html; }
    location / { return 301 https://@MAIN@/; }
}

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;
    ssl_certificate /etc/letsencrypt/live/@MAIN_CERT@/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/@MAIN_CERT@/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    return 301 https://@MAIN@/;
}
EOF
sudo sed -i -e "s/@MAIN_CERT@/$MAIN_CERT/g" -e "s/@MAIN@/$MAIN/g" /etc/nginx/sites-available/palaver-catch-all

Aktiviere, prüfe und lade sie.

sudo ln -sf /etc/nginx/sites-available/palaver-catch-all /etc/nginx/sites-enabled/palaver-catch-all
sudo nginx -t && sudo systemctl reload nginx

Optional: Wildcard-Zertifikat

  • ein Zertifikat für die Hauptdomain und jeden Namen darunter (palaver.social, *.palaver.social)
  • certbot-DNS-Prüfung
  • palaver.social: certbot-Snap, Plugin certbot-dns-hetzner-cloud, Zugangsdaten /root/.secrets/hetzner.ini, Deploy-Hook systemctl reload nginx
  • nur ein certbot je Host

Liste die Zertifikate und die Namen auf, die sie abdecken.

sudo certbot certificates

Prüfe den Deploy-Hook des Wildcard-Zertifikats.

sudo grep -E 'authenticator|renew_hook|deploy_hook' /etc/letsencrypt/renewal/palaver.social.conf

Dateien

Pfad Inhalt
/etc/systemd/system/palaverd@.service Unit-Vorlage
/opt/palaver/instances/<name>/palaverd.env Konfiguration (nur Benutzer palaver-<name>)
/opt/palaver/etc/livekit.yaml ein Schlüssel je Server unter keys:
/etc/nginx/sites-available/palaver-<name> nginx-Site
/etc/nginx/snippets/palaver-web-<name>.conf Web-Client, CSP für die Domain von <name>
/etc/nginx/sites-available/palaver-catch-all unbekannte Namen (optional)
/var/log/nginx/palaver/<name>.access.log, <name>.error.log nginx-Logs (rotiert von Palavers /etc/logrotate.d/palaver)

Fehlersuche

doctor eines Servers.

sudo -u "$SRV_USER" /opt/palaver/bin/palaverd -env "$CONF/palaverd.env" doctor

Was palaverd meldet.

sudo journalctl --namespace=palaver -u "$UNIT" -n 100

Was nginx meldet.

sudo tail -n 50 "/var/log/nginx/palaver/$NAME.error.log"
Symptom Ursache
useradd: invalid user name Name zu lang (höchstens 24 Zeichen)
Sprache geht nur auf diesem Server nicht Schlüssel fehlt in livekit.yaml oder LiveKit nach Schritt 8 nicht neu gestartet
Sprache geht auf keinem Server livekit.yaml durch eine Änderung kaputt: sudo journalctl --namespace=palaver -u livekit -n 50
die Site eines anderen Servers antwortet server_name oder der Symlink in sites-enabled fehlt
Web-Client im Browser ohne Sprache CSP: wss://$DOMAIN fehlt in palaver-web-$NAME.conf