Skip to main content
Postgres writes to the pg_data named volume declared in docker-compose.yml. That volume outlives container recreation, docker compose up --build, docker compose down, host reboots, and upgrades of the pgvector/pgvector image. Two things remove it: docker compose down -v, and losing the machine. The same command takes caddy_data with it, so a rebuilt instance refetches its TLS certificates and spends part of the Let’s Encrypt issuance limit for your domain.

What gets backed up

These records exist only in this database, and a backup is the only way to get them back:
  • accounts and platform identities
  • installations and grants
  • registered OAuth clients
  • channel write policies
  • write limits and rate tiers
  • encrypted Slack credentials, tokens issued to your instance specifically
The Discord message index and its embeddings are rebuilt rather than restored. A fresh instance runs the backfill again:
  • it re-fetches message history from Discord
  • it generates new embeddings for each message chunk
  • it reads image attachments again through the vision model
The rebuild takes time proportional to the amount of history, and the embedding and image-reading calls are billed again. Losing the identity and policy records produces the failure .env.example already warns about for changing MOB_BASE_URL after launch: your instance is its own OAuth issuer, so every connected agent presents a grant against an identity that no longer exists and stops working at the next request. Recovery runs through the original install path, with an owner reinstalling the Discord and Slack apps and every member reconnecting their agent and reauthorizing Slack. Discord and Slack themselves are unaffected.

The backup and the .env are one artifact

slack_installation_credentials and slack_user_authorizations store ciphertext. The key that reads them is MOB_CREDENTIAL_ENCRYPTION_KEY, which lives in .env and never enters the database. Restoring a backup next to a different key produces rows no one can decrypt, and every affected installation has to reauthorize with Slack before search works again. Back up .env whenever you back up the database and keep the pair together.

Creating a backup

-T disables TTY allocation, which is what lets the same line run from cron.

Restoring

Restore before the api starts. The api applies any migration file not yet recorded in schema_migrations on boot, so a backup from an older release lands on a stopped stack and gets brought forward by the next start.
The target database needs the pgvector extension available. The pgvector/pgvector image ships it; a plain postgres image does not. Test the restore once against a throwaway machine before you need it. None of these failures is visible until you try:
  • a missing pgvector extension
  • a role mismatch
  • an encryption key that no longer matches the ciphertext

Getting the backup off the machine

A backup stored on the server covers a bad upgrade or a mistaken delete, and a machine failure destroys the backup together with the volume it protects. mob ships no backup service in docker-compose.yml because a container writing backups beside that volume sits in the same failure domain. Two arrangements leave that domain:
  • point MOB_DATABASE_URL at a managed Postgres with point-in-time recovery and remove the postgres service from the compose file
  • keep the local database and pipe each backup to object storage with restic or rclone, storing .env in the same repository