Skip to main content
Version: 3.1

Enable Backups

Turn on Operaide backups for an installed instance. After this page, your host runs one cron job that produces a full backup tar every night. Off-site transport is a separate step covered in the transport pages.

Prerequisites

  • An Operaide instance running on a host you control with shell access.
  • The image carries the backup tooling (any image built from this branch onward).
  • Root or sudo access on the host (docker compose and cron typically need it).
info

The pages in this section assume the compose service is named app, which matches the install templates and the current on-prem layout. Older on-prem guides used operaide as the service name. If your compose.yml still has service: operaide, pick one of:

  • Rename the service in compose.yml from operaide to app (clean fix; keeps every command in this section copy-paste ready). Run docker compose down && docker compose up -d after the rename.
  • Override per cron invocation with an inline env var:
    0 3 * * * OPERAIDE_SERVICE_NAME=operaide /path/to/<project>/backup/operaide-outer-backup.sh >> /var/log/operaide-backup.log 2>&1
    The same OPERAIDE_SERVICE_NAME env var also works in front of operaide-outer-restore.sh during recovery.

Step 1: Mount the backup directory and expose the UI

The container needs a bind-mount at /backups so the host can read and write the same backup files the container produces. The system-admin Backups page appears automatically once this mount is present, and stays hidden without it. Open the project's compose.yml and edit the app service:

services:
app:
# ... existing config ...
volumes:
- app:/app/bundle/programs/server/assets/app/
- uploads:/srv/uploads
- data:/data
- mongodb:/mongodb
- ./backup:/backups

The ./backup path is relative to the directory that contains compose.yml. New instances created via the install tooling already have the mount, so the Backups page is reachable for system admins as soon as the container runs. There is no env flag to set: the container detects the /backups mount at startup and enables the page from that.

Step 2: Restart the container to seed the scripts

docker compose up -d

The first start after Step 1 creates backup/, backup/backups/, backup/drafts/ on the host and copies three files into backup/:

backup/operaide-outer-backup.sh
backup/operaide-outer-restore.sh
backup/transfer.sh

Verify they are there:

ls -la backup/

The first two are version-locked to the running image and are overwritten on every container start. The transfer.sh is a stub seeded on the first start only and is yours to edit.

Step 3: Run the backup once by hand

Trigger a single backup to verify everything works before scheduling cron:

./backup/operaide-outer-backup.sh

You should see log lines like:

[outer-backup] triggering inner-backup via 'docker compose exec app'...
[inner-backup] attempting mongodump (pre) against ...
[inner-backup] rsync /data -> /backups/.staging-...
[inner-backup] attempting mongodump (post) against ...
[inner-backup] building tar bundle: ...
[outer-backup] outer-backup done: /backups/backups/full-backup-...tar

A new tar appears under backup/backups/:

ls -la backup/backups/

If anything fails, the script writes the error to backup/last-backup-status.json and the system admin "Backups" page in Operaide shows a red banner. See the Reference page for the schema and exit codes.

Step 4: Configure off-site transport

Pick exactly one transport pattern. The backup tar is useless if it lives only on the same host that produced it.

The first three patterns edit transfer.sh and let outer-backup invoke the transport at the end of every run. The "bring your own" page covers the alternative where a separate cron job ships the tars off-site.

Step 5: Schedule the cron job

Add one entry to the host's crontab. The example below runs the backup every night at 03:00 local time:

sudo crontab -e
0 3 * * * /full/path/to/your/project/backup/operaide-outer-backup.sh >> /var/log/operaide-backup.log 2>&1

Replace /full/path/to/your/project with the absolute path to the directory that contains compose.yml. The cron job runs as root so it can talk to Docker; it does not need to switch users.

The script's lock file (backup/.outer-backup.lock) prevents two cron ticks from running in parallel. If a long backup is still running when the next tick fires, the second one logs "another outer-backup is in progress, skipping" and exits.

The cron line above appends to /var/log/operaide-backup.log forever. Drop a logrotate config so the file does not grow unbounded:

# /etc/logrotate.d/operaide-backup
/var/log/operaide-backup.log {
weekly
rotate 8
missingok
notifempty
compress
delaycompress
copytruncate
}

logrotate is installed by default on Debian, Ubuntu, and most other distributions; the daily cron at /etc/cron.daily/logrotate picks the file up automatically.

Step 6: Verify the next scheduled run

The morning after the first scheduled run, check three things:

  1. A new tar exists: ls -la backup/backups/. Most-recent file should match this morning's date.
  2. The status file says outer-done:
    jq '.stage' backup/last-backup-status.json
    Expected output: "outer-done".
  3. The Operaide UI confirms it: open System Admin → Backups in the web interface. The summary banner should be green ("Backups are healthy") and the Outer-block card should carry a "Transferred" badge if your transport pattern wrote the .transferred sidecar or removed the tar.

If something is off, see the Reference page for status codes and the Recovery Drill page for a non-destructive end-to-end check.

Common mistakes

  • Forgetting the bind-mount. Without ./backup:/backups, the container's inner-backup writes into a Docker named volume the host cannot see, and outer-backup fails to find the inner tar. The container logs WARN: /backups is not a mounted volume.
  • UI page hidden after enabling backups. The system-admin Backups page only registers when /backups is a mounted volume. If it stays hidden, confirm ./backup:/backups is in the app service and restart the container.
  • Relative path in cron. Cron's working directory is not your shell's current directory. Always use the absolute path to operaide-outer-backup.sh.
  • Editing the synced scripts. operaide-outer-backup.sh and operaide-outer-restore.sh get overwritten on every container start with the version from the image. Only transfer.sh is operator-owned and never overwritten after the initial seed.
  • No transport configured. A bundle on the same host as the original instance does not survive a host failure. Step 4 is mandatory, not optional.