Skip to content
Omicron

Self-hosting

Backups and restore

Automatic daily encrypted snapshots are built in. Two manual methods cover migrations and pre-upgrade snapshots.

Automatic backups (built in, on by default)

Every instance ships a backup service (pg_dump + restic) that needs no configuration: daily at 02:00 UTC it snapshots the database, uploads, state, and secrets into the encrypted backups volume, keeping the last 3 daily snapshots plus 1 weekly and pruning the rest. Snapshots are encrypted at rest (AES-256) with a password generated into the secrets volume on first boot.

What Default
Schedule Daily 02:00 UTC (BACKUP_SCHEDULE)
Destination Local backups volume (BACKUP_REPOSITORY)
Retention 3 daily + 1 weekly (BACKUP_KEEP_DAILY, BACKUP_KEEP_WEEKLY)
Contents Database dump, uploads, state, secrets, Caddyfile, bot policy
docker compose exec backup backup-now       # run one now (e.g. before upgrading)
docker compose exec backup restic snapshots # list snapshots
./backup/restore.sh                         # restore the latest snapshot
./backup/restore.sh 562229b2                # restore a specific snapshot

Going off-site (R2, S3, MinIO, B2)

Point the repository at any S3-compatible endpoint — only the endpoint and keys change, nothing else. No vendor SDK is involved and the format stays restic’s open one, so moving providers later is the same one-line change.

BACKUP_REPOSITORY=s3:https://<account-id>.r2.cloudflarestorage.com/omicron-backups
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

BACKUP_S3_REGION defaults to auto (what R2 wants); set a real region for AWS. Then docker compose up -d and one backup-now to confirm a snapshot lands. All variables are listed under Environment variables.

Manual methods

The automatic snapshots cover routine protection. The two manual methods below remain useful for host migrations and for a byte-for-byte copy before a risky upgrade. Both are verified end to end: seed, back up, destroy, restore, data intact.

All state lives in named volumes; nothing important is inside a container image.

Volume Holds In backups?
pgdata All database content Yes (Method B) or via dump (Method A)
uploads User-uploaded media Always
secrets Generated DB password + bootstrap session secret Method B only
state UI-rotated session secret, if any Method B only
caddy_data Let’s Encrypt certificates Optional
redis_data Transient queue state Never
backups Automatic restic snapshots (above) N/A — this is the backup

redis_data is deliberately excluded: it holds only pending jobs and in-flight federation deliveries, which Redis re-drains after a restart. No durable content lives there.

Commands assume the default Compose project name omicron (the directory name), so volumes are omicron_pgdata, omicron_uploads, and so on. Swap the prefix if you run with -p <name>. Everything works the same under podman — substitute the command.

Both methods below are verified end to end: seed, back up, destroy, restore, data intact.

Portable across Postgres major versions and safe to run on a live instance. On restore, the instance generates a fresh database password and session secret — so secrets is not backed up — which means everyone is signed out once. That is expected for a restore or migration.

Back up

# SQL dump + the two file volumes that hold real content.
docker compose exec -T postgres \
  pg_dump -U omicron -Fc omicron > omicron-db-$(date +%F).dump
for v in uploads caddy_data; do
  docker run --rm -v omicron_$v:/v:ro -v "$PWD":/backup alpine:3 \
    tar czf /backup/omicron-$v-$(date +%F).tgz -C /v .
done

Restore onto a fresh host

docker compose up -d postgres            # regenerates secrets, inits an empty DB
sleep 5
docker compose exec -T postgres \
  pg_restore -U omicron -d omicron --clean --if-exists < omicron-db-YYYY-MM-DD.dump
for v in uploads caddy_data; do
  docker run --rm -v omicron_$v:/v -v "$PWD":/backup alpine:3 \
    sh -c "rm -rf /v/* && tar xzf /backup/omicron-$v-YYYY-MM-DD.tgz -C /v"
done
docker compose up -d

Do not restore an old secrets tarball here — its db_password will not match the freshly initialised pgdata. caddy_data is optional; skipping it just re-issues certificates on first request.

Method B — full volume snapshot (simple, version-locked)

Copies every volume as-is, so pgdata and its matching secrets stay consistent and sessions survive. Tied to the same Postgres major version, and you must stop the stack first so the database files are copied at rest.

Back up

docker compose stop
for v in pgdata uploads state secrets caddy_data; do
  docker run --rm -v omicron_$v:/v:ro -v "$PWD":/backup alpine:3 \
    tar czf /backup/omicron-$v-$(date +%F).tgz -C /v .
done
docker compose start

Restore onto a fresh host

for v in pgdata uploads state secrets caddy_data; do
  docker run --rm -v omicron_$v:/v -v "$PWD":/backup alpine:3 \
    sh -c "tar xzf /backup/omicron-$v-YYYY-MM-DD.tgz -C /v"
done
docker compose up -d

Restore the whole set or none of it.

Choosing between them

Method A Method B
Runs on a live instance Yes No — requires a stop
Portable across Postgres versions Yes No
Sessions survive restore No Yes
Backup contains your secrets No Yes
Simplicity One dump + two tarballs Five tarballs

Method A for routine backups and host migrations. Method B when you want a byte-for-byte snapshot before a risky upgrade.

Automating it

The built-in daily snapshots above are the automation — no cron to write. If you disabled the sidecar (BACKUP_ENABLED=false), Method A from host cron is the fallback:

0 4 * * * cd /opt/omicron && docker compose exec -T postgres \
  pg_dump -U omicron -Fc omicron > /var/backups/omicron-$(date +\%F).dump

Test your restores

A backup you have never restored is a hypothesis. Restore into a throwaway VM or a second Compose project (-p omicron-test) at least once, and confirm you can sign in and read a post.

Found a mistake?Edit this page on GitHub.