Skip to main content
Version: 3.1

Restore from a Backup

Bring an Operaide instance back online from a full-backup tar. The standard path is one interactive command. The manual path runs the same steps explicitly when you want to inspect or override each one.

warning

Restore is a destructive operation. It writes the bundle's compose, env, and data into the target project directory. On a fresh host with no running Operaide there is no downtime. On a host that already runs Operaide, the script stops the instance first, then asks how to handle the existing data: delete the volumes and restore fresh, restore under a new project name (keeping the old volumes), or abort. Plan a maintenance window for in-place recovery and inform users.

Prerequisites

  • Docker and docker compose installed on the target host, plus jq, tar, and sha256sum. The script checks these and stops if any is missing.
  • The full-backup tar reachable on the host (mounted, scp'd, or downloaded from your transport target).
  • Optional but recommended: the matching .sha256 sidecar next to the tar so the script can verify integrity before extracting.
  • Root or sudo access (Docker typically requires it).

The bundle is self-contained. You do not need network access to a registry, an existing Operaide instance on the host, or any of the original deployment configuration outside the tar.

Standard path: outer-restore.sh

The bundle ships operaide-outer-restore.sh at its top level. Run it against the tar:

bash operaide-outer-restore.sh /path/to/full-backup-YYYYMMDD-HHMMSS.tar

If you have already extracted the tar, run the script from inside the extracted directory with no argument:

cd /path/to/extracted-bundle/
bash operaide-outer-restore.sh

The script first prints a prerequisites block (required host tools, free disk, that it stops the target itself) and waits for confirmation. Then it prompts for a few decisions:

PromptDefaultWhat it does
Continue with the restore?noConfirms you read the prerequisites block. Defaults to no, so an accidental run stops here.
Target project directory to restore intooriginal install pathDirectory the script writes compose.yml, .env, and backup/ into. Defaults to the path the backup was taken from (recorded in meta-outer.json), or ~/operaide if that is absent.
Restore into this directory?yesConfirms the resolved absolute path before any write.
Existing-data choice (o / n / a)abortShown only when the target's volumes already hold data. o deletes the volumes (docker compose down -v) and restores fresh, n restores under a new project name and keeps the old volumes, a aborts.
Load the docker image bundled in this backupyesRuns docker load on the embedded image tar. Say no only if you already have the exact image locally.
Which MongoDB dump to restore (pre/post)?postPicks mongodb-dump-post (freshest) or mongodb-dump-pre (consistent with the /data snapshot). See the section index for the trade-off.
Start the app now (docker compose up -d)?yesStarts the restored instance after the data has been loaded. Say no if you want to edit .env or compose.yml first.

Press Enter on each prompt to accept the default, or type your answer. The script stops the target install with docker compose down before restoring, and aborts if the bundle's sha256 does not match.

When the script finishes, the instance is running on the configured ROOT_URL. Open the URL in a browser to verify.

Manual path: step-by-step

Use this when:

  • You want to inspect each step (e.g. check the bundle contents before loading the image).
  • The standard path failed at a known step and you want to resume from there.
  • You need to deviate from the script's defaults (different volume names, custom service definition, etc.).

The manual path runs exactly the same operations as the script.

Step 1: verify the bundle

cd /path/to/where/the/tar/lives/
sha256sum -c full-backup-YYYYMMDD-HHMMSS.tar.sha256

Expected output: full-backup-YYYYMMDD-HHMMSS.tar: OK. If the verification fails, do not proceed; the bundle is corrupt and restoring from it would yield broken data.

Step 2: extract the bundle

mkdir restore-staging
tar -xf full-backup-YYYYMMDD-HHMMSS.tar -C restore-staging
ls -la restore-staging/

You should see operaide-image.tar, compose.yml, .env, inner-backup-*.tar.gz, operaide-outer-restore.sh, transfer.sh, and meta-outer.json at the top level.

Step 3: load the docker image

docker load -i restore-staging/operaide-image.tar

Output ends with the image tag the bundle was produced from. Confirm:

docker images | head

If you already have the same image locally, this step is a no-op.

Step 4: set up the target project directory

Pick a directory the restored instance will live in. The example uses ~/operaide. The directory does not need to exist yet:

TARGET=~/operaide
mkdir -p "$TARGET" "$TARGET/backup/drafts" "$TARGET/backup/backups"

Copy the configuration files into it:

cp restore-staging/compose.yml "$TARGET/"
cp restore-staging/.env "$TARGET/" # if present in the bundle
cp restore-staging/.env.openid "$TARGET/" # if present
cp restore-staging/operaide-outer-restore.sh "$TARGET/backup/"
cp restore-staging/transfer.sh "$TARGET/backup/"

Stage the inner-backup tar where the container will see it via the bind-mount:

cp restore-staging/inner-backup-*.tar.gz "$TARGET/backup/drafts/"
cp restore-staging/inner-backup-*.tar.gz.sha256 "$TARGET/backup/drafts/"

Step 5: ensure the data volumes are empty

The restore script refuses to overwrite a populated install. Check Docker named volumes referenced by the bundle's compose.yml:

docker volume ls | grep -E "(_data|_mongodb)"

If volumes from a previous restore exist, remove them:

cd "$TARGET"
docker compose down -v 2>/dev/null

Step 6: apply the inner backup

This is the destructive step: /data, app-assets, and MongoDB get written from the bundle. There are two layers of control. They produce the same end state. Pick the one that matches how much you want to drive yourself.

Both layers use --entrypoint to bypass the image's default start-production.sh startup. Without that, the application would write into /data/uploads before the restore runs, which trips the inner-restore safety check.

INNER_TAR="inner-backup-YYYYMMDD-HHMMSS.tar.gz"
cd "$TARGET"

Run a one-shot container whose entrypoint is operaide-restore-init.sh. It boots an embedded mongod, runs operaide-inner-restore.sh, and exits:

docker compose run --rm --no-deps \
-e OPERAIDE_RESTORE_MONGO_DUMP=post \
--entrypoint /usr/local/bin/operaide-restore-init.sh \
app \
"/backups/drafts/$INNER_TAR"

Set OPERAIDE_RESTORE_MONGO_DUMP=pre to use the start-of-run mongo dump instead of the end-of-run one.

You will see log lines for mongod startup, SHA verification, /data extraction, app-assets restore, and mongorestore. The container exits when restore is done.

Layer B: full manual via shell

Use this when you want to manage mongod's lifecycle yourself (point at an external MongoDB, leave mongod running afterwards for inspection, etc.) or do the restore step-by-step. Open an interactive shell with --entrypoint bash:

docker compose run --rm --no-deps \
-e OPERAIDE_RESTORE_MONGO_DUMP=post \
--entrypoint bash \
app

You land in a /srv shell with all the volumes mounted but no application process running and no mongod. From there:

Start mongod (skip if MONGO_URL already points at an external instance):

mkdir -p /var/log/mongodb
mongod --dbpath /mongodb --bind_ip 127.0.0.1 --port 27017 \
--logpath /var/log/mongodb/mongod-restore.log --quiet &
# wait until accept connections:
until mongosh --quiet --eval "db.runCommand({ping:1})" mongodb://127.0.0.1:27017 >/dev/null 2>&1; do sleep 1; done

Then either run inner-restore as one command:

/usr/local/bin/operaide-inner-restore.sh /backups/drafts/$INNER_TAR

Or do its work by hand (use this for partial restores, custom volume layout, or debugging). Each command matches one step inside operaide-inner-restore.sh:

INNER_TAR=/backups/drafts/inner-backup-YYYYMMDD-HHMMSS.tar.gz
STAGING=/tmp/inner-restore-manual
MONGO_TARGET="${MONGO_URL:-mongodb://127.0.0.1:27017/meteor}"
MONGO_BASE_URI="${MONGO_TARGET%/*}"

# 1. verify the inner-tar sha256
( cd "$(dirname "$INNER_TAR")" && sha256sum -c "$(basename "$INNER_TAR").sha256" )

# 2. extract to staging
mkdir -p "$STAGING"
tar -xzf "$INNER_TAR" -C "$STAGING"

# 3. restore /data
cp -a "$STAGING/data/." /data/

# 4. overlay VACUUM'd SQLite snapshots (if present)
[ -d "$STAGING/sqlite-vacuum" ] && cp -a "$STAGING/sqlite-vacuum/." /data/

# 5. restore app-assets (if present)
[ -d "$STAGING/app-assets" ] && cp -a "$STAGING/app-assets/." /app/bundle/programs/server/assets/app/

# 6. restore the chosen mongo dump (post = freshest, pre = filesystem-consistent)
mongorestore --uri="$MONGO_BASE_URI" --dir="$STAGING/mongodb-dump-post" --drop

# 7. clean up staging
rm -rf "$STAGING"

Stop mongod and exit (skip the shutdown if you started against an external MongoDB):

mongosh --quiet admin --eval "db.shutdownServer()" mongodb://127.0.0.1:27017
exit

The one-shot container is removed thanks to --rm. The volumes hold the restored state.

Step 7: start the app

cd "$TARGET"
docker compose up -d
docker compose logs -f app

Watch the logs until the application reports it is listening. Open the configured ROOT_URL to verify.

Verify the restore

After either path:

  1. Login still works. Sign in with your operator account. If the account uses an OIDC provider, compose.yml plus .env.openid are restored, so the federation should work without further setup.
  2. Apps and data are present. Open one of your apps. The reaktors, deployments, conversations, and uploaded documents should all be there.
  3. Logos render. Operator-uploaded logos live in the app-assets payload. They should appear in the same places they did before.
  4. System Admin -> Backups is healthy. The status snapshot reflects the new instance's first state. Run a fresh backup to confirm the cycle works on the new host.

Restore on the original host

The procedure is identical when you restore in place (same host, same project directory). The script stops the running instance itself, then detects the populated volumes and offers the existing-data choice. Pick o (overwrite) to delete the old volumes and restore fresh, or n to restore under a new project name and keep the old volumes as a fallback. You no longer clear the volumes by hand for the standard path.

Trust model

Restore trusts the bundle. Verification stops at the sha256 sidecar, which only proves the tar matches the sidecar that travelled with it. There is no signature chain back to the source host: an attacker who can swap both the tar and the sidecar at the off-site target can substitute a malicious bundle.

What the scripts do to limit the blast radius if a substituted bundle slips through:

  • tar --no-same-owner --no-same-permissions on every extract, so the bundle cannot plant suid-root binaries by re-using the source-host UIDs.
  • GNU tar's default rejection of absolute paths and ../ traversals (no -P flag is set), so the bundle cannot write outside EXTRACT_DIR.

What you, the operator, are responsible for:

  • Encrypt the bundle in transit and at rest. See Backup and Restore > "Encryption is your responsibility" for the layered guidance.
  • Restrict who can write to the off-site location. Treat the storage box / S3 bucket / bastion target as production-secret.
  • Optionally sign bundles with GPG or cosign if your threat model requires end-to-end integrity (not built into the scripts).

Common mistakes

  • Skipping the SHA check. A corrupt bundle that "looks restorable" produces silent data loss. Always verify the sha256 before extraction or rely on the script to do it.
  • Expecting a populated install to be overwritten silently. The script never restores on top of existing data. When it finds populated volumes it asks you to choose: overwrite (delete the volumes), restore under a new project name, or abort.
  • Using pre dump without reason. The post dump is newer and the right default. The pre dump exists for cases where the post dump is corrupt or you specifically need filesystem-mongo consistency at backup-start.
  • Assuming a different target directory isolates the volumes. Docker volumes are keyed by the name: project field in compose.yml, not by the directory. To run alongside an existing install, use the n (new project name) choice, which gives the restore fresh volumes.
  • Running outer-restore.sh as a different user than docker. The script invokes docker compose run and docker compose up. If your shell user is not in the docker group, run with sudo.