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
| Path | Origin | Purpose |
|---|---|---|
/usr/local/bin/operaide-inner-backup.sh | image | Container-side backup, called via docker compose exec. |
/usr/local/bin/operaide-inner-restore.sh | image | Container-side restore, called by restore-init. |
/usr/local/bin/operaide-restore-init.sh | image | One-shot recovery container entrypoint. Starts mongod, runs inner-restore. |
/srv/scripts/operaide-outer-backup.sh | image | Source for the host-side backup script. Synced to /backups/. |
/srv/scripts/operaide-outer-restore.sh | image | Source for the host-side restore script. Synced to /backups/. |
/srv/scripts/transfer.sh | image | Source for the transport stub. Seeded once into /backups/. |
/backups/ | bind-mount | Customer's <project>/backup/ directory. |
/var/lock/operaide-backup.lock | runtime | Container-internal flock used by inner-backup. |
Self-bootstrap rules
start-production.sh runs at every container start. When /backups is a mounted volume:
| Source script | Destination | Sync rule |
|---|---|---|
/srv/scripts/operaide-outer-backup.sh | /backups/operaide-outer-backup.sh | Overwrite when source is newer than destination, or destination is missing. Regenerate sha256 sidecar. |
/srv/scripts/operaide-outer-restore.sh | /backups/operaide-outer-restore.sh | Same as above. |
/srv/scripts/transfer.sh | /backups/transfer.sh | Copy 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
| Value | Meaning |
|---|---|
inner-done | Inner-backup finished, outer-backup has not yet bundled (transient). |
outer-done | Bundle written, sha256 computed, status file updated. Healthy state. |
failed | Either inner or outer aborted. errorMessage populated. |
Inner block fields
| Field | Type | Meaning |
|---|---|---|
startedAt | ISO 8601 UTC | When inner-backup acquired its lock. |
finishedAt | ISO 8601 UTC | When inner-backup wrote the inner tar. |
durationSeconds | integer | finishedAt minus startedAt. |
exitCode | integer | Inner-backup process exit. 0 = success. |
errorMessage | string or null | Last fail() message when exitCode is non-zero. |
Outer block fields
| Field | Type | Meaning |
|---|---|---|
startedAt | ISO 8601 UTC | When outer-backup acquired its lock. |
finishedAt | ISO 8601 UTC | When outer-backup wrote the bundle and sha256. |
durationSeconds | integer | finishedAt minus startedAt. |
exitCode | integer | Outer-backup process exit. 0 = success. |
fullBackupPath | string or null | Path 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. |
fullBackupSize | integer or null | Bundle size in bytes. |
fullBackupSha256 | string or null | Hex-encoded sha256 of the bundle. |
errorMessage | string or null | Last 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>.transferredexists 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.
| Value | Trigger |
|---|---|
never-run | last-backup-status.json does not exist. |
failed | stage is failed. |
disk-full | Backup volume disk usage is at or above 90 percent. |
in-progress | stage is inner-done and the status file is under 1 hour old. Outer phase is still bundling. |
incomplete | stage is inner-done and the status file is 1 hour or older. Outer phase never finished. |
transport-stuck | Oldest tar without .transferred sidecar in backup/backups/ is older than 7 days. |
stale | last-backup-status.json mtime is older than 25 hours. |
degraded | Run completed but warnings is non-empty. For example a failed VACUUM or a skipped mongodump. |
healthy | All checks pass and no warnings. |
Command-line flags
Outer-backup (operaide-outer-backup.sh)
| Flag | Effect |
|---|---|
--skip-image | Skip 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, --help | Print 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)
| Variable | Default | Effect |
|---|---|---|
MONGO_URL | mongodb://127.0.0.1:27017/meteor | Mongo connection used for both mongodump runs. |
OPERAIDE_BACKUP_MIN_FREE_BYTES | 5368709120 (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)
| Variable | Default | Effect |
|---|---|---|
MONGO_URL | mongodb://127.0.0.1:27017/meteor | Mongo target for mongorestore. |
OPERAIDE_RESTORE_MONGO_DUMP | post | Which dump to restore from: post (freshest) or pre (consistent with /data). Falls back to whichever exists if the requested one is missing. |
OPERAIDE_RESTORE_FORCE | 0 | Escape 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)
| Variable | Default | Effect |
|---|---|---|
OPERAIDE_SERVICE_NAME | app | Compose service name used for docker compose exec. Override only if your compose file renames the service. |
Outer-restore (operaide-outer-restore.sh)
| Variable | Default | Effect |
|---|---|---|
OPERAIDE_SERVICE_NAME | app | Compose 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:
| Code | Meaning | Source |
|---|---|---|
| 0 | Success, or another run holds the lock and this one skipped. | All scripts. |
| 64 | Usage error (missing argument, invalid value). | All scripts. |
| 65 | Bundle structure invalid. | inner-restore, outer-restore. |
| 66 | Required input file does not exist. | All scripts. |
| 70 | Internal scripting error (missing helper, bad image layout). | restore-init. |
| 71 | Required mount or path is missing. | inner-backup, restore-init. |
| 73 | Refused to overwrite a populated install. | inner-restore, outer-restore. |
| 74 | SHA-256 verification failed. | inner-restore, outer-restore. |
| 75 | Subprocess failed (mongodump, mongorestore, restore-init, disk-space eviction). | All scripts. |
| 76 | Internal logic error (no inner-backup tar found, image detection failed). | outer-backup. |
| 78 | Required host command not in $PATH. | outer-backup, outer-restore. |
| 130 | Interrupted by SIGINT. | restore-init. |
| 143 | Terminated by SIGTERM. | restore-init. |
Sources
The implementation lives at:
shared/admin/bin/operaide-inner-backup.shshared/admin/bin/operaide-inner-restore.shshared/admin/bin/operaide-restore-init.shshared/admin/scripts/operaide-outer-backup.shshared/admin/scripts/operaide-outer-restore.shshared/admin/scripts/transfer.shshared/admin/bin/start-production.sh(self-bootstrap)shared/meteor_server/imports/extensions/backup-status/(System Admin page)