Backup & Restore
What Persists, and What to Back Up
Section titled “What Persists, and What to Back Up”A Compose deployment keeps state in six named Docker volumes, plus your object storage bucket and your configuration files. scripts/backup.sh has a flag for only three of those locations, and just one of the six volumes. The table below covers every location and gives each one a verdict, so nothing is left out silently.
| Persistent location | Back it up? | Script flag | Details |
|---|---|---|---|
postgres_data |
Yes. This is the critical backup. | --db |
PostgreSQL data at /var/lib/postgresql/data |
| Object storage | Yes, when configured. | --storage |
The bucket configured by S3_ENDPOINT and S3_BUCKET |
| Configuration | Yes. | --config |
.env, certificates, and Compose files. Encrypted with tar and openssl |
api_data |
Yes, manually. | None | Patch compliance report CSV files at /data/patch-reports |
redis_data |
No. Preserve the volume, but do not back it up and do not restore a snapshot. | None | Rebuildable Redis state at /data |
binaries |
No. | None | Agent, viewer, and helper binaries at /data/binaries, mounted read-only. Refilled on every start |
caddy_data |
Usually no. | None | Caddy’s ACME account key and issued TLS certificates. See the note below |
caddy_config |
No. | None | Caddy’s autosaved runtime config, rewritten from docker/Caddyfile.prod on start |
./scripts/backup.sh --all runs --db, --storage, and --config. It does not touch any of the named volumes.
The api_data Volume
Section titled “The api_data Volume”The api_data volume is mounted at /data on the api service, and on the worker service when you run the worker-split profile. Its only persistent content is patch compliance reports, written as plain CSV files under /data/patch-reports. PostgreSQL stores the path to each file, so if the volume is lost the database keeps report rows whose files no longer exist and downloading one fails.
A report can be run again, but the replacement uses the current patch state. It does not reproduce the historical snapshot captured by the original CSV file.
scripts/backup.sh has no flag for this volume. It shells out to pg_dump, the S3 client and openssl, and reading a Docker volume needs access to the Docker socket, so the volume is handled as a separate command rather than a fourth flag. Copy it out with a throwaway container, writing into the same $BACKUP_DIR the script uses:
docker run --rm -v breeze_api_data:/data -v "${BACKUP_DIR:-/var/backups/breeze}":/backup alpine \ tar czf /backup/api_data_$(date +%Y%m%d).tar.gz -C /data .Compose prefixes the volume with the project name. The example assumes COMPOSE_PROJECT_NAME=breeze. Confirm the actual name before running the command:
docker volume lsRestore with the stack stopped. Extracting a tar archive over a volume overwrites the files it contains but does not delete anything else, so any report written after the backup was taken survives the restore and ends up mixed in with the restored files. Clear the directory first if you want the volume to match the archive exactly:
export API_DATA_ARCHIVE="${BACKUP_DIR:-/var/backups/breeze}/api_data_YYYYMMDD.tar.gz"docker compose down
# Optional: start from an empty directory so the restore is exact.docker run --rm -v breeze_api_data:/data alpine \ sh -c 'rm -rf /data/patch-reports'
docker run --rm -v breeze_api_data:/data -v "$(dirname "$API_DATA_ARCHIVE")":/backup alpine \ tar xzf "/backup/$(basename "$API_DATA_ARCHIVE")" -C /datadocker compose up -dBoth commands run as root inside the throwaway alpine container, so tar preserves the numeric owner and the API process (uid 1001) can still read the files afterwards.
Custom deployments must set PATCH_REPORT_STORAGE_PATH to a persistent location. Its default, ./data/patch-reports, is relative, so it resolves against the API process’s working directory rather than against the volume. In the published image that working directory is /app/apps/api, putting the reports in the container’s own writable layer, where they are lost the moment the container is replaced. The shipped Compose files set the absolute /data/patch-reports, and that override is what places the files on api_data.
The redis_data Volume
Section titled “The redis_data Volume”The redis_data volume is mounted at /data on Redis. Redis uses AOF persistence, so the volume survives container restarts. Preserve it across upgrades, and do not run docker compose down -v.
Do not add redis_data to a backup rotation, and do not restore it from a snapshot. PostgreSQL and Redis do not have a consistent snapshot mechanism. Restoring stale Redis state can reintroduce jobs that PostgreSQL records as complete.
Starting Redis empty loses:
- Queued and in-flight jobs, including work waiting for a retry. The next scheduled tick re-derives work from PostgreSQL, so this delays work instead of causing permanent data loss.
- Access-token revocation entries. A revoked but unexpired access token can be accepted again for at most 15 minutes for a normal session or 2 hours for a remote-desktop or viewer token.
- Rate-limit counters.
- Redis pub/sub messages that were in flight.
Starting Redis empty does not lose recurring schedules. They live in PostgreSQL and are registered again when the API and worker start. It also does not lose refresh-token family revocation, which is durable in PostgreSQL and is preserved by a database backup.
If the access-token window matters, such as after off-boarding an administrator, sign the affected users out again after the stack starts. This writes fresh revocation entries to the new Redis state.
The binaries Volume
Section titled “The binaries Volume”Do not back up the binaries volume. The binaries-init container rebuilds it on every docker compose up -d, and the API mounts it read-only. With the default BINARY_SOURCE=github, the API syncs binaries from GitHub Releases instead of using this volume.
Prerequisites
Section titled “Prerequisites”# PostgreSQL client toolssudo apt-get install postgresql-client-16
# psql is used by the restore verification steppsql --version
# MinIO Client (for --storage)curl -O https://dl.min.io/client/mc/release/linux-amd64/mcchmod +x mc && sudo mv mc /usr/local/bin/
# openssl (typically pre-installed)openssl versionEnvironment Variables
Section titled “Environment Variables”Set before running backup scripts:
# Required for --dbexport DATABASE_URL="postgresql://breeze:password@localhost:5432/breeze"
# Required for --configexport BACKUP_ENCRYPTION_KEY="a-strong-passphrase-at-least-32-chars"
# Required for --storageexport S3_ENDPOINT="http://localhost:9000"export S3_BUCKET="breeze"export S3_ACCESS_KEY="minioadmin"export S3_SECRET_KEY="minioadmin"
# Optionalexport BACKUP_DIR="/var/backups/breeze" # default: /var/backups/breezeexport BACKUP_RETENTION_DAYS="30" # default: 30Manual Backup
Section titled “Manual Backup”# Back up all script-managed components./scripts/backup.sh --all
# Database only./scripts/backup.sh --db
# Object storage only./scripts/backup.sh --storage
# Configuration only (encrypted)./scripts/backup.sh --configBackups are stored in $BACKUP_DIR with timestamps:
/var/backups/breeze/├── db_20260211_120000.dump├── storage_20260211_120000/└── config_20260211_120000.tar.gz.encAutomated Backups
Section titled “Automated Backups”Add a cron job for daily backups:
# Edit crontabcrontab -e
# Daily backup of all script-managed components at 2 AM0 2 * * * /path/to/breeze/scripts/backup.sh --all >> /var/log/breeze-backup.log 2>&1Restore
Section titled “Restore”# Restore database./scripts/restore.sh --db /var/backups/breeze/db_20260211_120000.dump
# Restore storage directory./scripts/restore.sh --storage /var/backups/breeze/storage_20260211_120000
# Restore configuration (prompts for encryption key)./scripts/restore.sh --config /var/backups/breeze/config_20260211_120000.tar.gz.encrestore.sh covers the same three components as backup.sh. To restore the api_data volume, use the tar command in The api_data Volume. Do not restore redis_data.
Retention
Section titled “Retention”Backups older than BACKUP_RETENTION_DAYS (default: 30) are automatically pruned during each backup run.
Offsite Backups
Section titled “Offsite Backups”For disaster recovery, sync backups to a remote location:
# Sync to S3/R2aws s3 sync /var/backups/breeze s3://breeze-backups/ --delete
# Sync to remote serverrsync -avz /var/backups/breeze/ backup-server:/backups/breeze/