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
sudoaccess on the host (docker composeandcrontypically need it).
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.ymlfromoperaidetoapp(clean fix; keeps every command in this section copy-paste ready). Rundocker compose down && docker compose up -dafter the rename. - Override per cron invocation with an inline env var:
The same
0 3 * * * OPERAIDE_SERVICE_NAME=operaide /path/to/<project>/backup/operaide-outer-backup.sh >> /var/log/operaide-backup.log 2>&1OPERAIDE_SERVICE_NAMEenv var also works in front ofoperaide-outer-restore.shduring 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.
- Rsync to a Hetzner Storage Box.
- Upload to S3 or compatible object storage.
- SCP through a bastion host.
- Bring your own backup tool for restic, Borg, or other agents.
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:
- A new tar exists:
ls -la backup/backups/. Most-recent file should match this morning's date. - The status file says outer-done:
Expected output:
jq '.stage' backup/last-backup-status.json"outer-done". - 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
.transferredsidecar 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 logsWARN: /backups is not a mounted volume. - UI page hidden after enabling backups. The system-admin Backups page only registers when
/backupsis a mounted volume. If it stays hidden, confirm./backup:/backupsis in theappservice 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.shandoperaide-outer-restore.shget overwritten on every container start with the version from the image. Onlytransfer.shis 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.