Skip to main content

Back up the whole installation

A database dump alone cannot recover uploaded logos, documents or gateway sessions. Preserve matching database and ERP storage backups, encryption/configuration secrets, the selected image versions and the proxy/tunnel configuration. For Evolution, additionally protect its PostgreSQL data and linked-session volume. R2 objects require their own backup/versioning policy; the local storage archive does not contain objects stored only in R2.

Backups can contain personal and financial information. Encrypt them before off-server transfer, restrict access and keep decryption/recovery keys separately. An archive listed in a bucket proves only presence—not a complete backup or a successful restore.

Installer backup helper​

The revised installer writes scripts/backup.sh. It creates matching SQL/storage archives, checks archive readability and uses restrictive file permissions. Its local output is not encrypted or automatically off-site. Arrange encryption, remote copies and failure/space monitoring yourself before relying on it.

cd /opt/nusaas
./scripts/backup.sh

The revised helper does not delete old backups by default. Setting BACKUP_RETENTION_DAYS in the helper's process environment explicitly enables age-based deletion of matching local SQL/storage archives. Only enable that after verifying remote copies and restore coverage; .env is not automatically sourced by the shell helper. Existing installations retain their old helper scripts until deliberately updated—review them for older automatic-retention behavior.

For a manual installation, a starting point is:

#!/usr/bin/env bash
set -euo pipefail
umask 077
cd /opt/nusaas
BACKUP_DIR=/var/backups/nusaas
STAMP=$(date -u +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
docker compose exec -T db sh -c \
'MYSQL_PWD="$MYSQL_PASSWORD" mysqldump --single-transaction -u"$MYSQL_USER" "$MYSQL_DATABASE"' \
| gzip > "$BACKUP_DIR/database-$STAMP.sql.gz"
docker compose exec -T api tar -C /var/www/storage -czf - . \
> "$BACKUP_DIR/storage-$STAMP.tar.gz"
gzip -t "$BACKUP_DIR/database-$STAMP.sql.gz"
tar -tzf "$BACKUP_DIR/storage-$STAMP.tar.gz" >/dev/null

This is not an application-consistent snapshot while writes continue. Schedule a maintenance window or use a reviewed snapshot/quiescing procedure where matching database/files require it. Include optional Compose overlays in commands for that installation. Preserve .env, web/.env.frontend, APP_KEY, JWT/Reverb secrets and recovery keys in an encrypted recovery set—not a public backup directory.

Private R2 archives​

Configure a dedicated private backup bucket using BACKUP_R2_*, independently of upload storage. Set an archive password and a base64-encoded 32-byte BACKUP_RECOVERY_KEY; preserve both offline and do not casually rotate them. Encrypted password-recovery records must match their archive; an older archive may lack one. See Cloudflare storage configuration.

Do not assume the scheduled application backup, local helper and optional gateway backups cover the same resources. Record which job produced each archive, whether the job succeeded and which isolated restore exercised it.

User-initiated updates​

There is no Watchtower or automatic application-image update. Review the release, confirm both published image tags/digests and take a recoverable database/storage backup first. Keep the previous images available. API startup applies forward migrations; application rollback does not undo them.

For the unmodified base installer stack:

./scripts/backup.sh
# Replace with a reviewed, published semantic version.
./scripts/update.sh YOUR_PUBLISHED_VERSION

The helper changes the pinned version, pulls images, restarts, applies migrations and checks API health. On failure it attempts to restore the previous version, but schema/data recovery is a separate operator decision. Do not rerun broad seeders during upgrades.

For Tunnel/Evolution or any customized Compose stack, adapt the update helper before use so every command includes the same Compose files; the base helper does not discover overlays automatically. Review gateway updates/licensing separately. After environment changes, recreate affected services; restart alone does not reload env_file.

Isolated restore exercise​

Use an isolated installation, separate database/volumes, separate hostnames and disabled outbound integrations. Never test a restore over production or reuse a live WhatsApp session against its production gateway.

  1. Record source backup timestamp, image versions, configuration/recovery-key availability and restore start time.
  2. Recover the matching SQL dump and full ERP storage, restore ownership/storage links and apply only reviewed forward migrations required by those image versions.
  3. Recover R2 objects and optional gateway resources according to their own procedures. Prevent restored jobs, payment callbacks, mail and messaging from contacting real customers/providers.
  4. Check sign-in, tenant data, representative invoices and file retrieval. Check that encrypted credentials can be decrypted without exposing their values.
  5. Record actual completion time, missing resources and verification results. Calculate recovery point/time from observed evidence, not archive-upload timestamps alone.

Do not use docker compose down -v to troubleshoot a live system; it deletes named volumes. Retention, deletion and recovery actions require deliberate operator decisions.