Skip to main content
Version: 3.1

Reference

Dry catalog for the backup system. File paths, the status JSON schema, environment variables, and exit codes.

File layout on the host

<project>/
compose.yml
.env
.env.openid
backup/
operaide-outer-backup.sh cron target, version-locked to image
operaide-outer-restore.sh disaster-recovery wrapper, version-locked
transfer.sh operator-owned transport hook
.outer-backup.lock flock file, persists across runs
last-backup-status.json current health (inner + outer blocks)
drafts/
inner-backup-YYYYMMDD-HHMMSS.tar.gz transient, deleted after bundle
inner-backup-YYYYMMDD-HHMMSS.tar.gz.sha256
backups/
full-backup-YYYYMMDD-HHMMSS.tar
full-backup-YYYYMMDD-HHMMSS.tar.sha256
full-backup-YYYYMMDD-HHMMSS.tar.transferred optional, set by transport

File layout inside the container

PathOriginPurpose
/usr/local/bin/operaide-inner-backup.shimageContainer-side backup, called via docker compose exec.
/usr/local/bin/operaide-inner-restore.shimageContainer-side restore, called by restore-init.
/usr/local/bin/operaide-restore-init.shimageOne-shot recovery container entrypoint. Starts mongod, runs inner-restore.
/srv/scripts/operaide-outer-backup.shimageSource for the host-side backup script. Synced to /backups/.
/srv/scripts/operaide-outer-restore.shimageSource for the host-side restore script. Synced to /backups/.
/srv/scripts/transfer.shimageSource for the transport stub. Seeded once into /backups/.
/backups/bind-mountCustomer's <project>/backup/ directory.
/var/lock/operaide-backup.lockruntimeContainer-internal flock used by inner-backup.

Self-bootstrap rules

start-production.sh runs at every container start. When /backups is a mounted volume:

Source scriptDestinationSync rule
/srv/scripts/operaide-outer-backup.sh/backups/operaide-outer-backup.shOverwrite when source is newer than destination, or destination is missing. Regenerate sha256 sidecar.
/srv/scripts/operaide-outer-restore.sh/backups/operaide-outer-restore.shSame as above.
/srv/scripts/transfer.sh/backups/transfer.shCopy only when destination is missing. Operator edits are preserved across container restarts.

If /backups is not a mounted volume, the application starts normally and logs a warning. Operator-driven backups are unavailable until the bind-mount is added.

Bundle layout

The full-backup tar produced by operaide-outer-backup.sh:

full-backup-YYYYMMDD-HHMMSS.tar
meta-outer.json
compose.yml
.env (if present)
.env.openid (if present)
operaide-image.tar (docker save output; omitted with --skip-image)
operaide-outer-restore.sh
operaide-outer-restore.sh.sha256 (if present in backup/)
transfer.sh (if present in backup/)
inner-backup-YYYYMMDD-HHMMSS.tar.gz
inner-backup-YYYYMMDD-HHMMSS.tar.gz.sha256

The inner-backup tar inside the bundle:

inner-backup-YYYYMMDD-HHMMSS.tar.gz
meta.json
data/ (rsync of /data, live SQLite excluded)
app-assets/ (rsync of /app/bundle/programs/server/assets/app/, PDF cache excluded)
sqlite-vacuum/ (VACUUM INTO copies, mirrored relative path)
mongodb-dump-pre/ (MongoDB dump taken at start of run)
mongodb-dump-post/ (MongoDB dump taken at end of run)

last-backup-status.json schema

The outer-backup script updates this file at the end of every run. Inner-backup writes the inner block; outer-backup adds the outer block.

{
"schemaVersion": 1,
"stage": "outer-done",
"inner": {
"startedAt": "2026-05-04T03:00:00Z",
"finishedAt": "2026-05-04T03:01:42Z",
"durationSeconds": 102,
"exitCode": 0,
"errorMessage": null
},
"outer": {
"startedAt": "2026-05-04T03:01:42Z",
"finishedAt": "2026-05-04T03:02:18Z",
"durationSeconds": 36,
"exitCode": 0,
"fullBackupPath": "backups/full-backup-20260504-030000.tar",
"fullBackupSize": 53687091200,
"fullBackupSha256": "abc123...",
"errorMessage": null
},
"warnings": []
}

stage values

ValueMeaning
inner-doneInner-backup finished, outer-backup has not yet bundled (transient).
outer-doneBundle written, sha256 computed, status file updated. Healthy state.
failedEither inner or outer aborted. errorMessage populated.

Inner block fields

FieldTypeMeaning
startedAtISO 8601 UTCWhen inner-backup acquired its lock.
finishedAtISO 8601 UTCWhen inner-backup wrote the inner tar.
durationSecondsintegerfinishedAt minus startedAt.
exitCodeintegerInner-backup process exit. 0 = success.
errorMessagestring or nullLast fail() message when exitCode is non-zero.

Outer block fields

FieldTypeMeaning
startedAtISO 8601 UTCWhen outer-backup acquired its lock.
finishedAtISO 8601 UTCWhen outer-backup wrote the bundle and sha256.
durationSecondsintegerfinishedAt minus startedAt.
exitCodeintegerOuter-backup process exit. 0 = success.
fullBackupPathstring or nullPath of the bundle relative to the backup-root bind-mount (e.g. backups/full-backup-...tar). The Backups page resolves it against the reader's BACKUP_ROOT for display and existence checks. Bundles produced by older scripts may carry an absolute path; the reader keeps those verbatim.
fullBackupSizeinteger or nullBundle size in bytes.
fullBackupSha256string or nullHex-encoded sha256 of the bundle.
errorMessagestring or nullLast fail() message when exitCode is non-zero.

warnings

Array of strings. Inner-backup appends here for non-fatal issues (failed VACUUM on a single SQLite file, mongodump skipped because mongo unreachable, evicted oldest backup to make space, etc.). Outer-backup does not write to this array.

Transferred-state signals

A bundle counts as transferred when either of these holds:

  • A file <bundle>.transferred exists next to it. Empty file is enough.
  • The bundle file itself is gone (transport tool moved it off-site).

The system admin Backups page derives the "Transferred" badge from these signals. The "transport stuck" warning fires after 7 days when neither signal is present.

Health values

The Backups page in System Admin computes a health field from the snapshot file plus filesystem state. The first matching condition wins, in the order listed.

ValueTrigger
never-runlast-backup-status.json does not exist.
failedstage is failed.
disk-fullBackup volume disk usage is at or above 90 percent.
in-progressstage is inner-done and the status file is under 1 hour old. Outer phase is still bundling.
incompletestage is inner-done and the status file is 1 hour or older. Outer phase never finished.
transport-stuckOldest tar without .transferred sidecar in backup/backups/ is older than 7 days.
stalelast-backup-status.json mtime is older than 25 hours.
degradedRun completed but warnings is non-empty. For example a failed VACUUM or a skipped mongodump.
healthyAll checks pass and no warnings.

Command-line flags

Outer-backup (operaide-outer-backup.sh)

FlagEffect
--skip-imageSkip docker save. The bundle omits operaide-image.tar. Smaller and faster, but no longer self-contained: restore must pull the compose image tag from the registry.
-h, --helpPrint the script header and exit.

The inner and restore scripts take no flags. Outer-restore detects a skipped image from the missing operaide-image.tar and falls back to the compose tag.

Environment variables

Inner-backup (operaide-inner-backup.sh)

VariableDefaultEffect
MONGO_URLmongodb://127.0.0.1:27017/meteorMongo connection used for both mongodump runs.
OPERAIDE_BACKUP_MIN_FREE_BYTES5368709120 (5 GiB)Minimum free space on /backups. Older full-backups are evicted to meet this, but the script refuses to evict the only remaining tar.

Inner-restore (operaide-inner-restore.sh)

VariableDefaultEffect
MONGO_URLmongodb://127.0.0.1:27017/meteorMongo target for mongorestore.
OPERAIDE_RESTORE_MONGO_DUMPpostWhich dump to restore from: post (freshest) or pre (consistent with /data). Falls back to whichever exists if the requested one is missing.
OPERAIDE_RESTORE_FORCE0Escape hatch for running operaide-inner-restore.sh directly. Set to 1 to skip the /data/uploads safety check and drop the MongoDB databases present in the selected dump before restoring them. The host wrapper sets this only after the operator chooses overwrite, so external MongoDB restores receive the same destructive consent as Docker volume restores.

Outer-backup (operaide-outer-backup.sh)

VariableDefaultEffect
OPERAIDE_SERVICE_NAMEappCompose service name used for docker compose exec. Override only if your compose file renames the service.

Outer-restore (operaide-outer-restore.sh)

VariableDefaultEffect
OPERAIDE_SERVICE_NAMEappCompose service name used for docker compose run. Override only if your compose file renames the service.

All other settings come from interactive prompts during the restore.

Exit codes

The inner and outer scripts use exit codes loosely aligned with sysexits.h:

CodeMeaningSource
0Success, or another run holds the lock and this one skipped.All scripts.
64Usage error (missing argument, invalid value).All scripts.
65Bundle structure invalid.inner-restore, outer-restore.
66Required input file does not exist.All scripts.
70Internal scripting error (missing helper, bad image layout).restore-init.
71Required mount or path is missing.inner-backup, restore-init.
73Refused to overwrite a populated install.inner-restore, outer-restore.
74SHA-256 verification failed.inner-restore, outer-restore.
75Subprocess failed (mongodump, mongorestore, restore-init, disk-space eviction).All scripts.
76Internal logic error (no inner-backup tar found, image detection failed).outer-backup.
78Required host command not in $PATH.outer-backup, outer-restore.
130Interrupted by SIGINT.restore-init.
143Terminated by SIGTERM.restore-init.

Sources

The implementation lives at:

  • shared/admin/bin/operaide-inner-backup.sh
  • shared/admin/bin/operaide-inner-restore.sh
  • shared/admin/bin/operaide-restore-init.sh
  • shared/admin/scripts/operaide-outer-backup.sh
  • shared/admin/scripts/operaide-outer-restore.sh
  • shared/admin/scripts/transfer.sh
  • shared/admin/bin/start-production.sh (self-bootstrap)
  • shared/meteor_server/imports/extensions/backup-status/ (System Admin page)