Skip to content
MeridFlow AiFlow v8.x • self-hosted

Backup and restore

Everything durable in an AiFlow deployment (the database, and every uploaded or extracted context document) lives in one place on the server. This page covers the command-line procedure for backing it up and restoring it. This isn't an HTTP endpoint: it runs on the server itself, inside the backend container, so the examples below are shell commands rather than cURL, Python, or TypeScript requests.

What gets backed up

  • The database: every Agent, Orchestrator, Trigger, Call, Event, ContextDocument row, and everything else this site documents.
  • If your deployment uses SQLite (the default), the backup takes a live, consistent snapshot using SQLite's own .backup command. This is safe to run while the backend is serving traffic: it is never a raw file copy, which could capture the database mid-write and produce a corrupted backup.
  • If your deployment uses PostgreSQL instead (an opt-in overlay covered in your deployment's own setup documentation), the backup uses pg_dump in its custom format.
  • Context documents: every file uploaded or imported as agent knowledge, packaged as a timestamped .tar.gz archive alongside the database backup.
  • Orchestrator sessions: the separate store holding each Orchestrator run and, more importantly, the state an Orchestrator saved under the user and app scopes. Those outlive the conversation that wrote them, and nothing else holds a copy. Backed up whenever the file exists, which is once a deployment has run an Orchestrator at all.

What does not get backed up

  • Credentials. Your deployment's environment configuration (API keys, the signing secret, Twilio and email provider credentials) is not part of this backup. Back up that configuration file the same way you back up any other secret.
  • The audit log is included as part of the main database, same as every other table. There is no separate step for it.

Running a backup

From the server, inside the running backend container:

docker compose exec backend python -m scripts.backup --output-dir /app/backend/backups
Flag Required Meaning
--output-dir No (default ./backups) Local directory the backup files are written to.
--rclone-remote No (default: none) An rclone remote:path to also copy the backup to (S3, Backblaze, a second server, or any other rclone-supported destination). Requires rclone to already be installed and configured (rclone config) on the machine running the backup; it is not bundled into the AiFlow image, since which remote (if any) you use is entirely your own choice.

Each run prints the path of every file it created. Two files are produced per run: the database backup, and a context_docs-<timestamp>.tar.gz archive.

To also copy the backup off the server:

docker compose exec backend python -m scripts.backup \
  --output-dir /app/backend/backups --rclone-remote myremote:aiflow-backups

Scheduling it

A cron entry on the host machine is the simplest way to run this unattended, for example, daily at 3 AM:

0 3 * * * docker compose -f /path/to/docker-compose.prod.yml exec -T backend \
  python -m scripts.backup --output-dir /app/backend/backups --rclone-remote myremote:aiflow-backups

Restoring

Stop the backend first if you are restoring a SQLite-backed deployment. Restoring over a database file the running process still has open is unsafe and can corrupt it.

docker compose stop backend
docker compose run --rm backend python -m scripts.restore \
  --db-backup /app/backend/backups/aiflow-20260101T030000Z.sqlite3 \
  --context-docs-backup /app/backend/backups/context_docs-20260101T030000Z.tar.gz \
  --adk-sessions-backup /app/backend/backups/adk_orchestrator_sessions-20260101T030000Z.sqlite3
docker compose start backend
Flag Required Meaning
--db-backup Yes Path to a database backup file produced by scripts.backup.
--context-docs-backup No Path to the matching context_docs-*.tar.gz archive. If omitted, only the database is restored.

If your deployment uses PostgreSQL, the restore uses pg_restore --clean --if-exists, which is safe to run against a database that already contains (possibly stale) tables: it drops and recreates them rather than failing on a conflict.

Verifying a restore

After restoring, confirm the deployment is healthy and the data you expect is present:

curl -s https://api.your-domain.com/health
curl -s https://api.your-domain.com/api/v1/agents \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

httpx.get("https://api.your-domain.com/health").raise_for_status()
agents = httpx.get(
    "https://api.your-domain.com/api/v1/agents",
    headers={"Authorization": f"Bearer {admin_token}"},
).json()
print(f"Restored {len(agents)} agent(s)")
await fetch("https://api.your-domain.com/health");
const response = await fetch("https://api.your-domain.com/api/v1/agents", {
  headers: { Authorization: `Bearer ${adminToken}` },
});
const agents = await response.json();
console.log(`Restored ${agents.length} agent(s)`);

See Agents and Authentication for $ADMIN_TOKEN.