Platform
Learn
Developer docs User guide Quickstart Blog
Company
Services About Contact Links Get started

Backups and restores

Updated

A Raytha site has two stores of data: the PostgreSQL database and the file storage for media. Back up both, keep your configuration somewhere safe, and test a restore before you need one.

What to back up

WhatWhere it livesWhy it matters
The databasePostgreSQL, one database (raytha in the examples)Content and drafts and their revisions, content types, views, users and roles, themes and templates, site pages, menus, Raytha Functions, webhooks and delivery log, authentication schemes, settings, audit and email logs, and the data-protection keys that sign users in.
File storageThe raytha_user_uploads volume (/app/user-uploads) for Local; the bucket or container for S3 and Azure BlobThe bytes of every media-library file and every theme asset. The database holds only a row per file.
ConfigurationYour .env, Compose file or platform variablesConnection string, SMTP, storage credentials, proxy settings. Not stored by Raytha.

You do not need to back up the container image, the admin bundle, or any cache. Rebuild them from the same Raytha version.

The database also holds what makes a restored site work immediately. The data-protection keys are stored in the DataProtectionKeys table, so after a restore people who were signed in stay signed in and encrypted values still decrypt. API keys are stored as hashes; their holders keep using the same keys.

Back up PostgreSQL

Use the custom format, which is compressed and restores selectively. Against the Compose database from the repository (container raytha-db, user postgres):

docker exec raytha-db pg_dump -U postgres -Fc raytha > raytha-$(date +%F).dump

Against any PostgreSQL server, from a machine with the client tools installed:

pg_dump -h db.example.com -p 5432 -U raytha -Fc -f raytha-$(date +%F).dump raytha

The dump is consistent: pg_dump reads a single snapshot, so you can run it while Raytha is serving traffic. Match the client's major version to the server's (PostgreSQL 17 for the repository's Compose files).

Back up media

Local storage

Archive the volume, mounted read-only so the backup cannot change it:

docker run --rm -v raytha_user_uploads:/data:ro -v "$PWD":/backup alpine \
  tar czf /backup/uploads-$(date +%F).tgz -C /data .

If you set FILE_STORAGE_LOCAL_DIRECTORY to a host path, back up that directory with your usual file backup tool.

S3 and Azure Blob

Raytha does not back these up for you. Turn on your provider's own protection: object versioning and replication, or a scheduled copy to a second bucket. A copy with aws s3 sync, mc mirror or az storage blob download-batch works too. Keep object keys unchanged, because the database refers to files by key.

Order matters

The two backups are taken separately, so they cannot be one snapshot. Take the database dump first and the files second. Every file the dump refers to is then in the files backup; a file uploaded between the two is just an extra object that nothing points to.

A backup script

This script backs up the repository's Compose setup and removes backups older than 14 days. Override the variables for other setups, and drop step 2 for S3 or Azure Blob.

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="${BACKUP_DIR:-/var/backups/raytha}"
DB_CONTAINER="${DB_CONTAINER:-raytha-db}"
UPLOADS_VOLUME="${UPLOADS_VOLUME:-raytha_user_uploads}"
KEEP_DAYS="${KEEP_DAYS:-14}"
STAMP="$(date +%F-%H%M)"

mkdir -p "$BACKUP_DIR"

# 1. Database first, so every file the dump refers to is still in the files backup below.
docker exec "$DB_CONTAINER" pg_dump -U postgres -Fc raytha > "$BACKUP_DIR/db-$STAMP.dump"

# 2. Uploaded files (skip this step if you use S3 or Azure Blob).
docker run --rm -v "$UPLOADS_VOLUME":/data:ro -v "$BACKUP_DIR":/backup alpine \
  tar czf "/backup/uploads-$STAMP.tgz" -C /data .

# 3. Drop old backups.
find "$BACKUP_DIR" -name 'db-*.dump' -mtime +"$KEEP_DAYS" -delete
find "$BACKUP_DIR" -name 'uploads-*.tgz' -mtime +"$KEEP_DAYS" -delete

Run it nightly from cron, then copy BACKUP_DIR off the machine. A backup on the same disk does not survive the disk.

15 2 * * * /usr/local/bin/raytha-backup.sh

Treat backups as secrets

A database dump contains credentials, so encrypt it and restrict who can read it:

  • The SMTP password saved in the admin (OrganizationSettings.SmtpPassword) is stored as plain text.
  • JWT secret keys and SAML certificates for authentication schemes.
  • Webhook signing secrets (Webhooks.Secret), stored as plain text.
  • The data-protection keys. Someone with them can forge Raytha's sign-in cookies.
  • Personal data: user emails, audit and email logs. Log retention (Settings, Maintenance) decides how much accumulates; the default is 180 days.

API keys are the exception: only their hashes are stored.

Restore

  1. Stop the app so nothing writes during the restore.
  2. Recreate an empty database and load the dump (command below).
  3. Restore the files into the same storage location, under the same keys.
  4. Start the same Raytha version the backup came from. If you want a newer version, upgrade afterwards: a dump from an older version contains an older schema, and APPLY_PENDING_MIGRATIONS brings it forward on start.
  5. Check /healthz/ready and open the site. See Health checks.
docker compose stop app
docker exec raytha-db dropdb -U postgres --if-exists raytha
docker exec raytha-db createdb -U postgres raytha
docker exec -i raytha-db pg_restore -U postgres -d raytha --no-owner < raytha-2026-10-02.dump

# Local storage: unpack into the volume
docker run --rm -v raytha_user_uploads:/data -v "$PWD":/backup:ro alpine \
  tar xzf /backup/uploads-2026-10-02.tgz -C /data

docker compose start app

Use --no-owner when the restoring database user is not the one that created the dump. To restore into a different database name, change the name in createdb, pg_restore -d and your connection string together.

This is also the rollback for a failed upgrade.

Prove the backup works

An untested backup is a guess. Restore the latest dump into a scratch database, without touching production:

docker exec raytha-db createdb -U postgres raytha_check
docker exec -i raytha-db pg_restore -U postgres -d raytha_check --no-owner < raytha-2026-10-02.dump

docker exec raytha-db psql -U postgres -d raytha_check -At \
  -c 'select count(*) as users from "Users"' \
  -c 'select count(*) as content_items from "ContentItems"' \
  -c 'select "MigrationId" from "__EFMigrationsHistory" order by 1 desc limit 1'

docker exec raytha-db dropdb -U postgres raytha_check

The counts should match your site and the last migration should be the one your Raytha version expects. For a stronger test, start a second Raytha against the scratch database and a copy of the files on another port, sign in, and open a page that shows an image.

Gotchas

  • Restoring one half. A database restored without its files shows broken media; files restored without the database are orphans.
  • The Website URL travels with the database. If you restore to a new domain, update the Website URL in Settings, Configuration, or emails and API media links keep pointing at the old host.
  • Volumes are deleted by docker compose down -v. So is your database.
  • The Compose database password is changeme. Change it before the database is reachable from anywhere. See Deploy with Docker.
  • Managed PostgreSQL. Providers offer point-in-time recovery. Use it in addition to pg_dump, not instead: a logical dump is the portable format you can restore anywhere.

Next steps