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.txtis critical. If it is lost, a restoredsessions.dbis worthless — the app generates a new secret and every logged-in session is invalidated. It lives in./data/, so a./databackup already includes it. Never omit it.
Backup
Cold backup (recommended — simplest and always consistent)
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.