Skip to content

Operations

Day-to-day running of a Relay installation: backup, restore, health, logs, updates. All commands assume you are in the Relay directory (where docker-compose.yml lives) and the stack is up (docker compose up -d).

What to back up

Relay's entire state is four things:

What Where Why it matters
Databases + files ./data/ (bind mount) inbox.db (all conversations/customers), sessions.db, users.json, numbers.json, session-secret.txt, media/, branding/
WhatsApp pairings Docker volume wa_auth Reconnect numbers without re-scanning every QR
Secrets & config ./.env SESSION_SECRET etc. — without it, restored sessions are invalid
TLS certificates ./ssl/ Your HTTPS cert + key

Not backed up (rebuilt automatically): node_modules, the app-owned Chrome, the source (in git).

session-secret.txt is critical. If it is lost, a restored sessions.db is worthless — the app generates a new secret and every logged-in session is invalidated. It lives in ./data/, so a ./data backup already includes it. Never omit it.

Backup

A brief stop guarantees a WAL-consistent snapshot. Schedule it at a low-traffic hour.

cd /path/to/relay
TS=$(date +%F)
mkdir -p backups/$TS

docker compose stop app                       # brief pause; nginx can stay up
cp -a data       backups/$TS/data
cp -a .env       backups/$TS/.env
cp -a ssl        backups/$TS/ssl 2>/dev/null || true
# WhatsApp pairings live in a Docker volume (project-prefixed name — check with
# `docker volume ls | grep wa_auth`):
docker run --rm -v "$(basename "$PWD")_wa_auth":/v -v "$PWD/backups/$TS":/b \
  alpine tar czf /b/wa_auth.tar.gz -C /v .
docker compose start app

tar czf backups/$TS.tar.gz -C backups $TS && rm -rf backups/$TS
echo "backup → backups/$TS.tar.gz"

Copy the resulting backups/<date>.tar.gz off the server (another host or object storage). A backup that lives only on the same box does not protect against losing the box.

Hot backup (no downtime — if a pause is unacceptable)

Snapshot the live SQLite DBs through the app's own SQLite (WAL-safe), then copy the rest:

docker compose exec app node -e "require('better-sqlite3')('data/inbox.db').backup('data/inbox.backup.db').then(()=>process.exit(0),e=>{console.error(e);process.exit(1)})"
# then cp data/inbox.backup.db + data/*.json + data/session-secret.txt + data/media out,
# and tar the wa_auth volume as above (a QR re-scan is the fallback if a pairing is torn).

Verify a snapshot's integrity any time:

docker compose exec app node -e "console.log(require('better-sqlite3')('data/inbox.db').pragma('integrity_check'))"

Restore

On a fresh host with Relay checked out and not yet started:

cd /path/to/relay
tar xzf <date>.tar.gz                 # unpacks a <date>/ dir
cp -a <date>/data ./data
cp -a <date>/.env ./.env
cp -a <date>/ssl  ./ssl
docker compose up -d                  # creates the wa_auth volume
docker compose stop app
docker run --rm -v "$(basename "$PWD")_wa_auth":/v -v "$PWD/<date>":/b \
  alpine sh -c "cd /v && tar xzf /b/wa_auth.tar.gz"
docker compose start app

Numbers reconnect from the restored pairings in 30–60 s. Any that don't (a pairing was mid-write during a hot backup) just need a QR re-scan under Numbers.

Renewing your TLS certificate

A Let's Encrypt certificate (issued during Installation) expires every 90 days. Nothing renews it automatically — plan for this, or your HTTPS will start failing about three months after your first install. The nginx container permanently holds ports 80/443, so a plain certbot certonly --standalone re-run (the install-time command) will fail; briefly stop nginx to free the port instead:

cd /path/to/relay
certbot renew --pre-hook "docker compose stop nginx" --post-hook "docker compose start nginx"

This causes a few seconds of downtime while nginx is stopped — schedule it at a low-traffic hour. Automate it with a monthly cron job so you never have to remember:

# crontab -e — runs at 03:15 on the 1st of every month
15 3 1 * * cd /path/to/relay && certbot renew --pre-hook "docker compose stop nginx" --post-hook "docker compose start nginx" >> /var/log/relay-cert-renew.log 2>&1

certbot renew only actually renews when the certificate is within 30 days of expiring, so running it monthly is safe and idempotent. After a renewal, confirm with docker compose logs nginx and a curl -sf https://your-domain.com/healthz.

Health checks

docker compose ps                     # both services "running"/"healthy"
curl -sf https://your-domain.com/healthz   # → relay-ok

GET /healthz is unauthenticated and returns relay-ok with 200 — point a load balancer or uptime monitor at it.

Logs

docker compose logs -f app            # follow the app
docker compose logs --since 1h app    # last hour

Logs are phone-free by policy (a jid domain may appear, never a number). Bound their disk use with the json-file driver in docker-compose.yml:

    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "5" }

Updating

git pull
docker compose up -d --build          # rebuilds the app image, restarts

The pinned Chrome is baked into the image, so a rebuild never changes the browser version. Pairings (volume) and sessions (sessions.db) survive the restart. Take a backup first.