Skip to content

Backup and restore

A Frameleaf backup has two parts: the database and your files. You need both. The database records where every file is and everything you’ve added (albums, people, descriptions, edits and settings), and Frameleaf doesn’t rebuild it by scanning your folders.

A 3-2-1 strategy is a good rule: three copies of your data, on two kinds of storage, with one copy off-site.

Frameleaf backs up its database automatically to UPLOAD_LOCATION/backups. By default it creates one daily at 2:00 AM and keeps the last 14. Change the schedule and retention in Administration, then Settings, then Backup (the Database Dump Settings section), where you can also turn automatic dumps off.

Setting Default
Enable database dumps On
Schedule Every day at 2:00 AM
Amount of previous dumps to keep 14

Database backups contain metadata only, never photos or videos. They’re only useful together with a copy of your files.

The first time Frameleaf starts on a library created by the official Immich server, it saves a copy of the database before upgrading anything, and shows Getting Ready… meanwhile. The copy is kept in the same folder, named like immich-db-backup-<date>T<time>-pre-upgrade-v<version>-pg<version>.sql.gz, and marked Before upgrade in the backups list.

  • It doesn’t count toward the retention limit and is never removed automatically. Delete it yourself when you no longer need it.
  • The step is skipped when a complete database backup less than 24 hours old is already in the folder.
  • Restore it like any other backup. See Moving from Immich.

Any of these creates a backup straight away:

  • In Administration, then Maintenance, open Database backups and choose Create backup now.
  • In Settings, then Compute & jobs, choose Create job, then Back up the database.
  • In the classic Administration, then Job Queues page, click Create job, select Create Database Dump and click Confirm.

The backup appears in UPLOAD_LOCATION/backups and counts toward your retention limit. From the Database backups list you can also download or delete a backup.

Back up the whole of UPLOAD_LOCATION if you can. If you have to choose, these folders hold your originals and are critical:

  • UPLOAD_LOCATION/library
  • UPLOAD_LOCATION/upload
  • UPLOAD_LOCATION/profile

Also back up UPLOAD_LOCATION/thumbs. Thumbnails can be regenerated, but this folder also holds your edited versions and the edit masks and Clean Up fills the Frameleaf apps upload (files named *_develop_artifact_*.png), and those can’t be regenerated.

encoded-video holds re-encoded copies of videos and can be rebuilt. If you skip it, rerun the video transcoding job after a restore, and the thumbnail job if you skipped thumbs.

Each person has a folder named after their user ID inside most of these locations. You can find your user ID in Account settings, then Account.

What Where Can it be rebuilt?
Originals uploaded from the web, apps or CLI (storage template off, the default) UPLOAD_LOCATION/upload/<userID> No
Originals, when the storage template is on UPLOAD_LOCATION/library/<userID> No
Profile pictures UPLOAD_LOCATION/profile/<userID> No
Thumbnails, previews and face thumbnails UPLOAD_LOCATION/thumbs/<userID> Yes
Edited versions and develop artifacts UPLOAD_LOCATION/thumbs/<userID> No
Re-encoded videos (the original is kept) UPLOAD_LOCATION/encoded-video/<userID> Yes
Automatic database backups UPLOAD_LOCATION/backups Only from the live database
The database itself DB_DATA_LOCATION No, back it up as a dump

When the storage template is on, Frameleaf moves originals into library/, using the person’s storage label instead of the user ID if an administrator set one (the first administrator’s label is admin). Files from the mobile apps land in upload/<userID> first and move to library/ once the upload finishes. If you turn the storage template off again, existing files stay in library/ and new uploads go to upload/.

The safest backup is taken with the server container (the immich-server service) stopped, so nothing changes during the backup.

If you can’t stop it, back up the database first and the files second. The worst case is then a few files the database doesn’t know about, which you can upload again. The other way round, the restored database can point at files that aren’t in your backup, and those photos show as broken.

  1. Go to Administration, then Maintenance.
  2. Expand Restore database backup. Each backup shows its version, creation date and size.
  3. Click Restore next to the backup you want.
  4. Read the summary, type RESTORE to confirm, and start the restore. You can choose Create a safety backup of the current database first to keep a copy of what you have now.

Restoring replaces the current database. Metadata, albums, people, edits and settings return to the state in the backup, and changes made since are lost. Original files on disk aren’t touched. Everyone is signed out, and the server stays in maintenance mode until you end it.

The restore runs in four steps: Frameleaf enters maintenance mode, restores the database, runs any migrations and verifies the library. A restore point is taken first. If anything fails, such as a damaged backup or a backup with no administrator account, it rolls back to the restore point automatically.

To restore a backup file from elsewhere, click Select from computer in the same section and choose a .sql.gz file. It appears in the list with an uploaded- prefix.

  1. Set up .env and docker-compose.yml as in Install with Docker Compose.
  2. Move the old server’s backups, encoded-video, library, profile, thumbs and upload folders into the new UPLOAD_LOCATION.
  3. If you used external libraries, make sure the new Compose file mounts them the same way. You may need to move files to match.
  4. Run docker compose up -d.
  5. On the welcome screen, click Restore from backup. Frameleaf enters maintenance mode.
  6. Check the storage folder report. It shows whether each folder is readable and writable, and how many files it holds.
  7. Click Next, choose a backup from the list or upload a .sql.gz file, and click Restore.

For example, if the old server had UPLOAD_LOCATION=/my-broken-instance/media and the new one has UPLOAD_LOCATION=/a-brand-new-instance/data, move:

/my-broken-instance/media/backups -> /a-brand-new-instance/data/backups
/my-broken-instance/media/encoded-video -> /a-brand-new-instance/data/encoded-video
/my-broken-instance/media/library -> /a-brand-new-instance/data/library
/my-broken-instance/media/profile -> /a-brand-new-instance/data/profile
/my-broken-instance/media/thumbs -> /a-brand-new-instance/data/thumbs
/my-broken-instance/media/upload -> /a-brand-new-instance/data/upload

The backup list marks each backup as matching your current version, made with a different version, or of unknown version.

  • Older backup: the restore brings it up to date with the database migrations.
  • Newer backup: made by a newer server than the one you’re running. Update the server before restoring it.
  • Unknown version: if its migrations fail, the server rolls back to the current database.

Restore to a matching version when you can.

For scripted recovery you can back up and restore with pg_dump, gunzip and psql. Run these from your Compose folder, next to its .env file. The database service name works whatever the container is called. Replace <DB_USERNAME> and <DB_DATABASE_NAME> with the values in your .env (usually postgres and immich).

Back up:

Terminal window
docker compose exec -T database pg_dump --clean --if-exists \
--dbname=<DB_DATABASE_NAME> --username=<DB_USERNAME> \
| gzip > "/path/to/backup/dump.sql.gz"

Restore:

Terminal window
docker compose down -v # CAUTION: deletes all Frameleaf data to start from scratch
# rm -rf DB_DATA_LOCATION # CAUTION: uncomment and set your Postgres path to reset the database for good
docker compose pull # update to the latest version, if you want
docker compose create # create the containers without starting them
docker compose up -d database # start Postgres only
sleep 10 # give Postgres time to start
gunzip --stdout "/path/to/backup/dump.sql.gz" \
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
| docker compose exec -T database psql --dbname=<DB_DATABASE_NAME> --username=<DB_USERNAME> --single-transaction --set ON_ERROR_STOP=on
docker compose up -d # start the rest of Frameleaf

On Windows, the same steps work in PowerShell, except that you mount the dump file into the database service (for example - 'C:\path\to\backup\dump.sql:/dump.sql'), open a shell with docker compose exec database bash, and run the sed and psql part inside it. Use cat for a .sql file or gunzip --stdout for .sql.gz.

Good to know:

  • The restore needs a completely fresh database that the server has never started on. If the server has run, you’ll see errors such as “relation already exists”. Delete the DB_DATA_LOCATION folder to reset the database and try again.
  • If you can’t start the database without also starting the server, set DB_SKIP_MIGRATIONS=true during the restore so the server doesn’t run migrations that get in the way. Remove it and restart afterwards.
  • The commands restore in a single transaction, so the database is never left half-restored. To turn that off, remove --single-transaction --set ON_ERROR_STOP=on.
  • The backup and restore process changed in version 2.5.0. A backup made by an older server may need older manual instructions.

Borg is a deduplicating backup tool with built-in versioning. This script dumps the database into your upload folder, then backs up the folder, database dump included, to a second drive and to a remote computer. Because the dump and the files go into the same snapshot, they’re always in step. Run it daily or weekly from cron.

It assumes a second drive on the server for the on-site copy and SSH access to a remote computer for the off-site copy. Remove the remote lines if you don’t need them.

Before you start:

  • Install Borg on the server and on the remote computer.
  • To run the script as a non-root user, add that user to the docker group.
  • To run it unattended, set up passwordless SSH from the server to the remote computer, from the account that will run the script.

Set up the Borg repositories once:

Terminal window
UPLOAD_LOCATION="/path/to/frameleaf/directory" # as set in your .env file
BACKUP_PATH="/path/to/local/backup/directory"
mkdir "$UPLOAD_LOCATION/database-backup"
borg init --encryption=none "$BACKUP_PATH/frameleaf-borg"
# Remote set-up
REMOTE_HOST="remote_host@IP"
REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
borg init --encryption=none "$REMOTE_HOST:$REMOTE_BACKUP_PATH/frameleaf-borg"

Edit this script and add it to your crontab. It assumes your paths contain no :, @ or " characters.

#!/bin/sh
# Paths
UPLOAD_LOCATION="/path/to/frameleaf/directory"
BACKUP_PATH="/path/to/local/backup/directory"
REMOTE_HOST="remote_host@IP"
REMOTE_BACKUP_PATH="/path/to/remote/backup/directory"
### Local
# Back up the Frameleaf database
docker exec -t frameleaf_postgres pg_dump --clean --if-exists --dbname <DB_DATABASE_NAME> --username=<DB_USERNAME> > "$UPLOAD_LOCATION"/database-backup/frameleaf-database.sql
# Compressing makes deduplication less effective. If you use another tool or prefer to compress anyway:
# docker exec -t frameleaf_postgres pg_dump --clean --if-exists --dbname <DB_DATABASE_NAME> --username=<DB_USERNAME> | /usr/bin/gzip --rsyncable > "$UPLOAD_LOCATION"/database-backup/frameleaf-database.sql.gz
# Append to the local Borg repository
borg create "$BACKUP_PATH/frameleaf-borg::{now}" "$UPLOAD_LOCATION" --exclude "$UPLOAD_LOCATION"/encoded-video/
borg prune --keep-weekly=4 --keep-monthly=3 "$BACKUP_PATH"/frameleaf-borg
borg compact "$BACKUP_PATH"/frameleaf-borg
### Remote
borg create "$REMOTE_HOST:$REMOTE_BACKUP_PATH/frameleaf-borg::{now}" "$UPLOAD_LOCATION" --exclude "$UPLOAD_LOCATION"/encoded-video/
borg prune --keep-weekly=4 --keep-monthly=3 "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/frameleaf-borg
borg compact "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/frameleaf-borg

The script skips encoded-video, which can be rebuilt. It keeps thumbs, because that folder also holds edited versions and develop artifacts that can’t be regenerated.

To restore, mount a repository and copy back what you need:

Terminal window
BACKUP_PATH="/path/to/local/backup/directory"
mkdir /tmp/frameleaf-mountpoint
borg mount "$BACKUP_PATH"/frameleaf-borg /tmp/frameleaf-mountpoint
cd /tmp/frameleaf-mountpoint

For the remote copy, mount "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/frameleaf-borg instead. Each snapshot is a separate folder. When you’re done, run borg umount /tmp/frameleaf-mountpoint.

Cloud backup and Buddy backup keep an encrypted off-site copy of your originals, edits, settings and a verified database dump, and can run together. If the server itself is lost, you can bring it back from a cloud backup bucket without the web app using frameleaf-admin cloud-backup restore. See Server commands. Buddy backup has its own offline export and recovery commands, described on the Buddy backup page.

This applies to Cloud backup plans bought in the iPhone or Android app, which come in fixed sizes. Plans bought on the web add storage automatically instead.

When the backups no longer fit the plan and the owner keeps it, the backup storage becomes read-only and Backup paused: plan full shows on the server and in the Frameleaf app. Your library is untouched and restores keep working. New items wait until the plan is upgraded or the backup gets smaller, then backups carry on by themselves.

If the server is linked and push notifications are set up:

  • Your plan has outgrown its tier goes only to the server’s owner, because it shows plan and usage details. The owner is the administrator who signs in with the Frameleaf account the server is linked to.
  • Backup paused: plan full goes to every administrator, without plan details, each time the plan fills up. A Family Sharing member is asked to contact their family organiser.