docs: update immich_backup

This commit is contained in:
Administrator 2026-02-14 03:34:51 +00:00 committed by John Smith
parent 0df4b2965e
commit 7aa392c5b9

View file

@ -2,7 +2,7 @@
title: Immich Backup and Restore
description: Immich backup with Kopia
published: true
date: 2026-02-14T03:16:12.150Z
date: 2026-02-14T03:34:42.017Z
tags:
editor: markdown
dateCreated: 2026-02-14T03:14:32.594Z
@ -12,7 +12,7 @@ dateCreated: 2026-02-14T03:14:32.594Z
## Overview
This document provides comprehensive backup and recovery procedures for Immich photo management server. Following the same proven strategy as our Mailcow backups, we use a **two-tier approach** combining local component-level backups with Kopia for offsite storage in vaults.
This document provides comprehensive backup and recovery procedures for Immich photo server. Since Immich's data is stored on standard filesystems (not ZFS or BTRFS), snapshots are not available and we rely on Immich's native backup approach combined with Kopia for offsite storage in vaults.
## Quick Reference
@ -22,6 +22,10 @@ This document provides comprehensive backup and recovery procedures for Immich p
# Run a manual backup (all components)
/opt/scripts/backup-immich.sh
# Backup just the database
docker exec -t immich_postgres pg_dump --clean --if-exists \
--dbname=immich --username=postgres | gzip > "/opt/immich-backups/dump.sql.gz"
# List Kopia snapshots
kopia snapshot list --tags immich
@ -32,8 +36,9 @@ tail -f /var/log/immich-backup.log
### Common Restore Commands
```bash
# Restore from local backup (interactive)
/opt/immich-backups/immich-restore.sh /opt/immich-backups/immich-YYYYMMDD_HHMMSS/
# Restore database from backup
gunzip < /opt/immich-backups/immich-YYYYMMDD_HHMMSS/dump.sql.gz | \
docker exec -i immich_postgres psql --username=postgres --dbname=immich
# Restore from Kopia to new server
kopia snapshot list --tags tier1-backup
@ -46,284 +51,575 @@ docker compose logs -f
## Critical Components to Backup
### 1. Docker Compose Configuration
- **Location**: `/opt/immich/docker-compose.yml`
### 1. Docker Compose File
- **Location**: `/opt/immich/docker-compose.yml` (or your installation path)
- **Purpose**: Defines all containers, networks, and volumes
- **Importance**: Critical for recreating the exact container configuration
### 2. Environment Configuration
### 2. Configuration Files
- **Primary Config**: `/opt/immich/.env`
- **Purpose**: Database credentials, upload locations, API keys
- **Purpose**: Database credentials, upload locations, timezone settings
- **Importance**: Required for proper service initialization
### 3. PostgreSQL Database
- **Purpose**: Contains all metadata, user accounts, albums, sharing settings, face recognition data
- **Docker Volume**: `immich_postgres`
- **Backup Method**: `pg_dumpall` (hot backup, no downtime)
### 3. Database
- **PostgreSQL Data**: Contains all metadata, user accounts, albums, sharing settings, face recognition data, timeline information
- **Container**: `immich_postgres`
- **Database Name**: `immich` (default)
- **User**: `postgres` (default)
- **Backup Method**: `pg_dump` (official Immich recommendation)
### 4. Photo/Video Library
- **Purpose**: All original photos and videos uploaded by users
- **Docker Volume**: `immich_upload` or `immich_library`
- **Upload Storage**: All original photos and videos uploaded by users
- **Location**: `/srv/immich/library` (per your .env UPLOAD_LOCATION)
- **Size**: Typically the largest component
- **Critical**: This is your actual data - photos cannot be recreated
### 5. Additional Important Data
- **Thumbnails**: Can be regenerated but saves processing time
- **Encoded Video**: Transcoded versions, can be regenerated
- **Profile Pictures**: User avatars
- **Machine Learning Models**: Can be re-downloaded
- **Model Cache**: Docker volume `immich_model-cache` (machine learning models, can be re-downloaded)
- **External Paths**: `/export/photos` and `/srv/NextCloud-AIO` (mounted as read-only in your setup)
## Backup Strategy
### Two-Tier Backup Approach
We use a **two-tier approach** combining local snapshots with Kopia for offsite storage:
We use a **two-tier approach** combining Immich's native backup method with Kopia for offsite storage:
1. **Tier 1 (Local)**: Component-level backups (database dump + volume archives)
1. **Tier 1 (Local)**: Immich database dump + library backup creates consistent, component-level backups
2. **Tier 2 (Offsite)**: Kopia snapshots the local backups and syncs to vaults
#### Why This Approach?
- **Best of both worlds**: Local backups ensure quick component-level restore, Kopia provides deduplication and offsite protection
- **Component-level restore**: Can restore individual components (just database, just photos, etc.)
- **Best of both worlds**: Native database dump ensures Immich-specific consistency, Kopia provides deduplication and offsite protection
- **Component-level restore**: Can restore individual components (just database, just library, etc.)
- **Disaster recovery**: Full system restore from Kopia backups on new server
- **Efficient storage**: Kopia's deduplication reduces storage needs for offsite copies
- **Proven strategy**: Same approach used successfully for Mailcow backups
#### Backup Frequency
- **Daily**: Local Tier 1 backup runs at 2 AM
- **Daily**: Kopia Tier 2 snapshot runs immediately after Tier 1
- **Retention (Local)**: 7 days of local backups
- **Daily**: Immich backup runs at 2 AM
- **Daily**: Kopia snapshot of backups runs at 3 AM
- **Retention (Local)**: 7 days of Immich backups (managed by script)
- **Retention (Kopia/Offsite)**: 30 daily, 12 weekly, 12 monthly
### What Gets Backed Up
### Immich Native Backup Method
Each backup creates:
- **database.sql.gz**: Complete PostgreSQL dump
- **immich_*.tar.gz**: Compressed archives of each Docker volume
- **docker-compose.yml**: Container configuration
- **.env**: Environment variables and secrets
- **manifest.txt**: Backup inventory and sizes
- **checksums.sha256**: Integrity verification
Immich's official backup approach uses `pg_dump` for the database:
- Uses `pg_dump` with `--clean --if-exists` flags for consistent database dumps
- Hot backup without stopping PostgreSQL
- Produces compressed `.sql.gz` files
- Database remains available during backup
**Key Features:**
- No downtime required
- Consistent point-in-time snapshot
- Standard PostgreSQL format (portable across systems)
- Can be restored to any PostgreSQL instance
## Setting Up Immich Backups
### Prerequisites
### Prereq:
Make sure you are connected to the repository,
Connect to Kopia Repository:
```bash
```bash
sudo kopia repository connect server \
--url=https://192.168.5.10:51516 \
--override-username=admin \
--server-cert-fingerprint=YOUR_FINGERPRINT_HERE
--server-cert-fingerprint=696a4999f594b5273a174fd7cab677d8dd1628f9b9d27e557daa87103ee064b2
```
### Step 1: Configure Backup Location
#### Step 1: Configure Backup Location
Set the backup destination:
```bash
sudo mkdir -p /opt/immich-backups
sudo chown -R root:root /opt/immich-backups
sudo chmod 755 /opt/immich-backups
# Create the backup directory
mkdir -p /opt/immich-backups
chown -R root:root /opt/immich-backups
chmod 755 /opt/immich-backups
```
### Step 2: Install Backup Scripts
#### Step 2: Manual Backup Commands
```bash
sudo mkdir -p /opt/scripts
cd /opt/immich
sudo cp immich-backup.sh /opt/scripts/backup-immich.sh
sudo cp immich-restore.sh /opt/immich-backups/immich-restore.sh
# Backup database using Immich's recommended method
docker exec -t immich_postgres pg_dump \
--clean \
--if-exists \
--dbname=immich \
--username=postgres \
| gzip > "/opt/immich-backups/dump.sql.gz"
sudo chmod +x /opt/scripts/backup-immich.sh
sudo chmod +x /opt/immich-backups/immich-restore.sh
# Backup library (photos/videos)
tar -czf /opt/immich-backups/library.tar.gz -C /srv/immich library
# Backup configuration files
cp docker-compose.yml /opt/immich-backups/
cp .env /opt/immich-backups/
```
### Step 3: Configure Backup Script
**What gets created:**
- Backup directory: `/opt/immich-backups/immich-YYYY-MM-DD-HH-MM-SS/`
- Contains: `dump.sql.gz` (database), `library.tar.gz` (photos), config files
Edit `/opt/scripts/backup-immich.sh` and verify:
#### Step 3: Automated Backup Script
Create `/opt/scripts/backup-immich.sh`:
```bash
BACKUP_DIR="/opt/immich-backups"
RETENTION_DAYS=7
#!/bin/bash
# Immich Automated Backup Script
# This creates Immich backups, then snapshots them with Kopia for offsite storage
set -e
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/immich-backup.log"
IMMICH_DIR="/opt/immich"
BACKUP_DIR="/opt/immich-backups"
KEEP_DAYS=7
# Database credentials from .env
DB_USERNAME="postgres"
DB_DATABASE_NAME="immich"
POSTGRES_CONTAINER="immich_postgres"
PROJECT_NAME="immich"
ENABLE_KOPIA=true
echo "[${BACKUP_DATE}] ========================================" | tee -a "$LOG_FILE"
echo "[${BACKUP_DATE}] Starting Immich backup process" | tee -a "$LOG_FILE"
# Step 1: Run Immich database backup using official method
echo "[${BACKUP_DATE}] Running Immich database backup..." | tee -a "$LOG_FILE"
cd "$IMMICH_DIR"
# Create backup directory with timestamp
mkdir -p "${BACKUP_DIR}/immich-${BACKUP_DATE}"
# Backup database using Immich's recommended method
docker exec -t ${POSTGRES_CONTAINER} pg_dump \
--clean \
--if-exists \
--dbname=${DB_DATABASE_NAME} \
--username=${DB_USERNAME} \
| gzip > "${BACKUP_DIR}/immich-${BACKUP_DATE}/dump.sql.gz"
BACKUP_EXIT=${PIPESTATUS[0]}
if [ $BACKUP_EXIT -ne 0 ]; then
echo "[${BACKUP_DATE}] ERROR: Immich database backup failed with exit code ${BACKUP_EXIT}" | tee -a "$LOG_FILE"
exit 1
fi
echo "[${BACKUP_DATE}] Immich database backup completed successfully" | tee -a "$LOG_FILE"
# Step 2: Backup upload library (photos/videos)
echo "[${BACKUP_DATE}] Backing up upload library..." | tee -a "$LOG_FILE"
# Get the upload location from docker-compose volumes
UPLOAD_LOCATION="/srv/immich/library"
if [ -d "${UPLOAD_LOCATION}" ]; then
tar -czf "${BACKUP_DIR}/immich-${BACKUP_DATE}/library.tar.gz" \
-C "$(dirname ${UPLOAD_LOCATION})" \
"$(basename ${UPLOAD_LOCATION})" 2>&1 | tee -a "$LOG_FILE"
echo "[${BACKUP_DATE}] Upload library backup completed" | tee -a "$LOG_FILE"
else
echo "[${BACKUP_DATE}] WARNING: Upload location not found at ${UPLOAD_LOCATION}" | tee -a "$LOG_FILE"
fi
# Step 3: Backup configuration files
echo "[${BACKUP_DATE}] Backing up configuration files..." | tee -a "$LOG_FILE"
cp "${IMMICH_DIR}/docker-compose.yml" "${BACKUP_DIR}/immich-${BACKUP_DATE}/"
cp "${IMMICH_DIR}/.env" "${BACKUP_DIR}/immich-${BACKUP_DATE}/"
echo "[${BACKUP_DATE}] Configuration backup completed" | tee -a "$LOG_FILE"
# Step 4: Clean up old backups
echo "[${BACKUP_DATE}] Cleaning up backups older than ${KEEP_DAYS} days..." | tee -a "$LOG_FILE"
find "${BACKUP_DIR}" -maxdepth 1 -type d -name "immich-*" -mtime +${KEEP_DAYS} -exec rm -rf {} \; 2>&1 | tee -a "$LOG_FILE"
echo "[${BACKUP_DATE}] Local backup cleanup completed" | tee -a "$LOG_FILE"
# Step 5: Create Kopia snapshot of backup directory
echo "[${BACKUP_DATE}] Creating Kopia snapshot..." | tee -a "$LOG_FILE"
kopia snapshot create "${BACKUP_DIR}" \
--tags immich,tier1-backup \
--description "Immich backup ${BACKUP_DATE}" \
2>&1 | tee -a "$LOG_FILE"
KOPIA_EXIT=${PIPESTATUS[0]}
if [ $KOPIA_EXIT -ne 0 ]; then
echo "[${BACKUP_DATE}] WARNING: Kopia snapshot failed with exit code ${KOPIA_EXIT}" | tee -a "$LOG_FILE"
echo "[${BACKUP_DATE}] Local Immich backup exists but offsite copy may be incomplete" | tee -a "$LOG_FILE"
exit 2
fi
echo "[${BACKUP_DATE}] Kopia snapshot completed successfully" | tee -a "$LOG_FILE"
# Step 6: Also backup the Immich installation directory (configs, compose files)
echo "[${BACKUP_DATE}] Backing up Immich installation directory..." | tee -a "$LOG_FILE"
kopia snapshot create "${IMMICH_DIR}" \
--tags immich,config,docker-compose \
--description "Immich config ${BACKUP_DATE}" \
2>&1 | tee -a "$LOG_FILE"
echo "[${BACKUP_DATE}] Backup process completed successfully" | tee -a "$LOG_FILE"
echo "[${BACKUP_DATE}] ========================================" | tee -a "$LOG_FILE"
# Optional: Send notification on completion
# Add your notification method here (email, webhook, etc.)
```
Find your PostgreSQL container:
Make it executable:
```bash
docker ps | grep postgres
chmod +x /opt/scripts/backup-immich.sh
```
### Step 4: Test Manual Backup
Add to crontab (daily at 2 AM):
```bash
sudo /opt/scripts/backup-immich.sh
```
# Edit root's crontab
crontab -e
Verify:
```bash
ls -lh /opt/immich-backups/
kopia snapshot list --tags immich
```
### Step 5: Automated Backup with Cron
```bash
sudo crontab -e
```
Add for daily backups at 2 AM:
```
# Add this line:
0 2 * * * /opt/scripts/backup-immich.sh 2>&1 | logger -t immich-backup
```
### Step 6: Configure Kopia Retention
### Offsite Backup to Vaults
```bash
kopia policy set /opt/immich-backups \
--keep-latest 30 \
--keep-daily 30 \
--keep-weekly 12 \
--keep-monthly 12
kopia policy set /opt/immich \
--keep-latest 7 \
--keep-daily 7
```
After local Kopia snapshots are created, they sync to your offsite vaults automatically through Kopia's repository configuration.
## Recovery Procedures
### Understanding Two Recovery Methods
We have **two restore methods** depending on the scenario:
1. **Local Restore** (Preferred): For component-level or same-server recovery
2. **Kopia Full Restore**: For complete disaster recovery to a new server
### Method 1: Local Restore (Recommended)
Full system restore:
Use this method when:
- Restoring on the same/similar server
- Restoring specific components (just database, just library, etc.)
- Recovering from local Immich backups
#### Full System Restore
```bash
cd /opt/immich
# Stop Immich
docker compose down
# List available backups
ls -lh /opt/immich-backups/
sudo /opt/immich-backups/immich-restore.sh /opt/immich-backups/immich-YYYYMMDD_HHMMSS/
# Choose a backup
BACKUP_PATH="/opt/immich-backups/immich-YYYYMMDD_HHMMSS"
# Restore database
gunzip < ${BACKUP_PATH}/dump.sql.gz | \
docker compose exec -T database psql --username=postgres --dbname=immich
# Restore library
tar -xzf ${BACKUP_PATH}/library.tar.gz -C /srv/immich/
# Restore configuration (review changes first)
cp ${BACKUP_PATH}/.env .env.restored
cp ${BACKUP_PATH}/docker-compose.yml docker-compose.yml.restored
# Start Immich
docker compose up -d
# Monitor logs
docker compose logs -f
```
Database-only restore:
#### Example: Restore Only Database
```bash
cd /opt/immich
# Stop Immich
docker compose down
docker compose up -d postgres
# Start only database
docker compose up -d database
sleep 10
# Restore database from backup
BACKUP_PATH="/opt/immich-backups/immich-YYYYMMDD_HHMMSS"
gunzip < "${BACKUP_PATH}/database.sql.gz" | docker exec -i immich_postgres psql -U postgres
gunzip < ${BACKUP_PATH}/dump.sql.gz | \
docker compose exec -T database psql --username=postgres --dbname=immich
# Start all services
docker compose down
docker compose up -d
# Verify
docker compose logs -f
```
Volume-only restore:
#### Example: Restore Only Library
```bash
cd /opt/immich
# Stop Immich
docker compose down
# Restore library from backup
BACKUP_PATH="/opt/immich-backups/immich-YYYYMMDD_HHMMSS"
VOLUME_NAME="immich_upload"
docker run --rm \
-v "${VOLUME_NAME}:/data" \
-v "${BACKUP_PATH}:/backup" \
alpine sh -c "cd /data && tar -xzf /backup/${VOLUME_NAME}.tar.gz"
tar -xzf ${BACKUP_PATH}/library.tar.gz -C /srv/immich/
# Start Immich
docker compose up -d
```
### Method 2: Complete Server Rebuild
**Note**: For library restore, ensure proper permissions after extraction:
```bash
chown -R 1000:1000 /srv/immich/library
```
### Method 2: Complete Server Rebuild (Kopia Restore)
Use this when recovering to a completely new server or when local backups are unavailable.
#### Step 1: Prepare New Server
```bash
# Update system
apt update && apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com | sh
sudo systemctl enable docker
sudo apt install docker-compose-plugin -y
systemctl enable docker
systemctl start docker
# Install Docker Compose
apt install docker-compose-plugin -y
# Install Kopia
curl -s https://kopia.io/signing-key | sudo gpg --dearmor -o /usr/share/keyrings/kopia-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/kopia-keyring.gpg] https://packages.kopia.io/apt/ stable main" | sudo tee /etc/apt/sources.list.d/kopia.list
sudo apt update && sudo apt install kopia -y
apt update
apt install kopia -y
# Create directories
sudo mkdir -p /opt/immich /opt/immich-backups /opt/scripts
# Create directory structure
mkdir -p /opt/immich
mkdir -p /opt/immich-backups
mkdir -p /srv/immich/library
mkdir -p /srv/immich/postgres
```
#### Step 2: Connect to Kopia
#### Step 2: Restore Kopia Repository
```bash
sudo kopia repository connect server \
--url=https://YOUR_KOPIA_SERVER:51516 \
# Connect to your offsite vault
kopia repository connect server \
--url=https://192.168.5.10:51516 \
--override-username=admin \
--server-cert-fingerprint=YOUR_FINGERPRINT
--server-cert-fingerprint=696a4999f594b5273a174fd7cab677d8dd1628f9b9d27e557daa87103ee064b2
kopia repository status
# List available snapshots
kopia snapshot list --tags immich
```
#### Step 3: Restore Configuration
```bash
# Find and restore the config snapshot
kopia snapshot list --tags config
kopia restore <config-snapshot-id> /opt/immich/
ls -la /opt/immich/docker-compose.yml
# Restore to the Immich directory
kopia restore <snapshot-id> /opt/immich/
# Verify critical files
ls -la /opt/immich/.env
ls -la /opt/immich/docker-compose.yml
```
#### Step 4: Restore Backups
#### Step 4: Restore Immich Backups Directory
```bash
# Restore the entire backup directory from Kopia
kopia snapshot list --tags tier1-backup
# Restore the most recent backup
kopia restore <snapshot-id> /opt/immich-backups/
# Verify backups were restored
ls -la /opt/immich-backups/
```
#### Step 5: Run Local Restore
```bash
LATEST_BACKUP=$(ls -td /opt/immich-backups/immich-* | head -1)
sudo /opt/immich-backups/immich-restore.sh "$LATEST_BACKUP"
```
#### Step 6: Start and Verify
#### Step 5: Restore Database and Library
```bash
cd /opt/immich
# Find the most recent backup
LATEST_BACKUP=$(ls -td /opt/immich-backups/immich-* | head -1)
echo "Restoring from: $LATEST_BACKUP"
# Start database container
docker compose up -d database
sleep 30
# Restore database
gunzip < ${LATEST_BACKUP}/dump.sql.gz | \
docker compose exec -T database psql --username=postgres --dbname=immich
# Restore library
tar -xzf ${LATEST_BACKUP}/library.tar.gz -C /srv/immich/
# Fix permissions
chown -R 1000:1000 /srv/immich/library
```
#### Step 6: Start and Verify Immich
```bash
cd /opt/immich
# Pull latest images (or use versions from backup if preferred)
docker compose pull
# Start all services
docker compose up -d
# Monitor logs
docker compose logs -f
```
Verify:
- [ ] All containers running: `docker compose ps`
- [ ] Web interface accessible: `curl -I http://localhost:2283`
- [ ] Database accessible: `docker compose exec postgres psql -U postgres -c "\l"`
- [ ] Photos visible: `docker compose exec immich_server ls /usr/src/app/upload/`
- [ ] Login and check albums, timeline, sharing
#### Step 7: Post-Restore Verification
```bash
# Check container status
docker compose ps
# Test web interface
curl -I http://localhost:2283
# Verify database
docker compose exec database psql -U postgres -d immich -c "SELECT COUNT(*) FROM users;"
# Check library storage
ls -lah /srv/immich/library/
```
### Scenario 2: Restore Individual User's Photos
To restore a single user's library without affecting others:
```bash
cd /opt/immich
# Stop Immich temporarily
docker compose down
# Extract library to temporary location
BACKUP_PATH="/opt/immich-backups/immich-YYYYMMDD_HHMMSS"
mkdir -p /tmp/immich-restore
tar -xzf ${BACKUP_PATH}/library.tar.gz -C /tmp/immich-restore
# Find the user's directory (using user ID from database)
# User libraries are typically in: library/{user-uuid}/
USER_UUID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Copy user's data back
rsync -av /tmp/immich-restore/library/${USER_UUID}/ \
/srv/immich/library/${USER_UUID}/
# Fix permissions
chown -R 1000:1000 /srv/immich/library/${USER_UUID}/
# Cleanup
rm -rf /tmp/immich-restore
# Start Immich
docker compose up -d
# Trigger re-scan for this user via the web UI
```
### Scenario 3: Database Recovery Only
If only the database is corrupted but library data is intact:
```bash
cd /opt/immich
# Stop Immich
docker compose down
# Start only database
docker compose up -d database
sleep 30
# Restore from most recent backup
LATEST_BACKUP=$(ls -td /opt/immich-backups/immich-* | head -1)
gunzip < ${LATEST_BACKUP}/dump.sql.gz | \
docker compose exec -T database psql --username=postgres --dbname=immich
# Start all services
docker compose down
docker compose up -d
# Verify
docker compose logs -f
```
### Scenario 4: Configuration Recovery Only
If you only need to restore configuration files:
```bash
cd /opt/immich
# Find the most recent backup
LATEST_BACKUP=$(ls -td /opt/immich-backups/immich-* | head -1)
# Stop Immich
docker compose down
# Backup current config (just in case)
cp .env .env.pre-restore
cp docker-compose.yml docker-compose.yml.pre-restore
# Restore config from backup
cp ${LATEST_BACKUP}/.env ./
cp ${LATEST_BACKUP}/docker-compose.yml ./
# Restart
docker compose up -d
```
## Verification and Testing
Monthly backup verification:
### Regular Backup Verification
Perform monthly restore tests to ensure backups are valid:
```bash
# Verify local backups
ls -lth /opt/immich-backups/ | head -5
# Verify checksums
LATEST_BACKUP=$(ls -td /opt/immich-backups/immich-* | head -1)
cd "$LATEST_BACKUP" && sha256sum -c checksums.sha256
# Verify Kopia snapshots
# Test restore to temporary location
mkdir -p /tmp/backup-test
kopia snapshot list --tags immich
kopia restore <snapshot-id> /tmp/backup-test/
# Verify files exist and are readable
ls -lah /tmp/backup-test/
gunzip < /tmp/backup-test/immich-*/dump.sql.gz | head -100
# Cleanup
rm -rf /tmp/backup-test/
```
### Backup Monitoring Script
@ -333,157 +629,131 @@ Create `/opt/scripts/check-immich-backup.sh`:
```bash
#!/bin/bash
LAST_BACKUP=$(ls -td /opt/immich-backups/immich-* | head -1)
# Check last backup age
LAST_BACKUP=$(ls -td /opt/immich-backups/immich-* 2>/dev/null | head -1)
if [ -z "$LAST_BACKUP" ]; then
echo "ERROR: No backups found!"
echo "WARNING: No Immich backups found"
exit 1
fi
BACKUP_DATE=$(basename "$LAST_BACKUP" | sed 's/immich-//')
BACKUP_EPOCH=$(date -d "${BACKUP_DATE:0:8} ${BACKUP_DATE:9:2}:${BACKUP_DATE:11:2}:${BACKUP_DATE:13:2}" +%s 2>/dev/null)
if [ -z "$BACKUP_EPOCH" ]; then
echo "WARNING: Cannot parse backup date"
exit 1
fi
NOW=$(date +%s)
AGE_HOURS=$(( ($NOW - $BACKUP_EPOCH) / 3600 ))
if [ $AGE_HOURS -gt 26 ]; then
echo "WARNING: Last Immich backup is $AGE_HOURS hours old"
# Send alert (email, Slack, etc.)
exit 1
else
echo "OK: Last backup $AGE_HOURS hours ago"
BACKUP_SIZE=$(du -sh "$LAST_BACKUP" | cut -f1)
echo "Size: $BACKUP_SIZE"
fi
```
Add to cron:
```bash
chmod +x /opt/scripts/check-immich-backup.sh
sudo crontab -e
0 8 * * * /opt/scripts/check-immich-backup.sh | logger -t immich-backup-check
# Check Kopia snapshots
KOPIA_LAST=$(kopia snapshot list --tags immich --json 2>/dev/null | jq -r '.[0].startTime' 2>/dev/null)
if [ -n "$KOPIA_LAST" ]; then
echo "Last Kopia snapshot: $KOPIA_LAST"
else
echo "WARNING: Cannot verify Kopia snapshots"
fi
```
## Disaster Recovery Checklist
- [ ] Confirm scope of failure
- [ ] Access offsite Kopia repository
- [ ] Provision new server with Docker and Kopia
When disaster strikes, follow this checklist:
- [ ] Confirm scope of failure (server, storage, specific component)
- [ ] Gather server information (hostname, IP, DNS records)
- [ ] Access offsite backup vault
- [ ] Provision new server (if needed)
- [ ] Install Docker and dependencies
- [ ] Connect to Kopia repository
- [ ] Restore configuration from Kopia
- [ ] Restore backup directory from Kopia
- [ ] Run local restore script
- [ ] Start Immich containers
- [ ] Verify web interface and photo access
- [ ] Update DNS if needed
- [ ] Document issues and lessons learned
- [ ] Restore configurations first
- [ ] Restore database
- [ ] Restore library data
- [ ] Start services and verify
- [ ] Test photo viewing and uploads
- [ ] Verify user accounts and albums
- [ ] Update DNS records if needed
- [ ] Document any issues encountered
- [ ] Update recovery procedures based on experience
## Important Notes
1. **Machine Learning Models**: Can be excluded from backup (re-download automatically)
2. **Photo Deduplication**: Remains intact after restore
3. **Facial Recognition**: Stored in database, restored automatically
4. **Permissions**: Handled automatically by Docker
5. **Upgrades**: Always restore with same or newer Immich version
6. **DNS**: Update if restoring to new server IP
1. **External Mounts**: Your setup has `/export/photos` and `/srv/NextCloud-AIO` mounted as external read-only sources. These are not backed up by this script - ensure they have their own backup strategy.
## Troubleshooting
2. **Database Password**: The default database password in your .env is `postgres`. Change this to a secure random password for production use.
### "Database backup failed"
```bash
docker ps | grep postgres
docker compose logs postgres
docker exec immich_postgres pg_dumpall -U postgres | head
```
3. **Permissions**: Library files should be owned by UID 1000:1000 for Immich to access them properly:
```bash
chown -R 1000:1000 /srv/immich/library
```
### "Volume not found"
```bash
docker volume ls | grep immich
docker compose up -d --no-start
docker volume ls
```
4. **Testing**: Always test recovery procedures in a lab environment before trusting them in production.
### "Kopia snapshot fails"
```bash
kopia repository status
kopia repository connect server --url=...
kopia snapshot create /opt/immich-backups
```
5. **Documentation**: Keep this guide and server details in a separate location (printed copy, password manager, etc.).
### "Photos missing after restore"
```bash
docker compose exec immich_server ls -lah /usr/src/app/upload/
docker volume inspect immich_upload
docker compose logs -f immich_server
```
## Advanced Topics
### Exclude Components to Save Space
Edit backup script to exclude thumbnails/encoded videos:
```bash
docker volume ls --filter "name=${PROJECT_NAME}" --format "{{.Name}}" | grep -v "thumbs"
```
### Incremental Backups
```
# Daily: Database + config only
0 2 * * 1-6 /opt/scripts/backup-immich-db-only.sh
# Weekly: Full backup
0 2 * * 0 /opt/scripts/backup-immich.sh
```
### Backup Notifications
Add to end of backup script:
```bash
ADMIN_EMAIL="admin@example.com"
echo "Immich backup completed: ${TIER1_SIZE}" | \
mail -s "✓ Immich Backup Success" "$ADMIN_EMAIL"
curl -fsS --retry 3 https://hc-ping.com/your-uuid-here
```
6. **Retention Policy**: Review Kopia retention settings periodically to balance storage costs with recovery needs.
## Backup Architecture Notes
### Storage Efficiency Example
### Why Two Backup Layers?
For a 500GB photo library:
**Immich Native Backups** (Tier 1):
- ✅ Uses official Immich backup method (`pg_dump`)
- ✅ Fast, component-aware backups
- ✅ Selective restore (can restore just database or just library)
- ✅ Standard PostgreSQL format (portable)
- ❌ No deduplication (full copies each time)
- ❌ Limited to local storage initially
**Without Kopia**:
- 7 days × 500GB = 3.5TB local storage
**Kopia Snapshots** (Tier 2):
- ✅ Deduplication and compression
- ✅ Efficient offsite replication to vaults
- ✅ Point-in-time recovery across multiple versions
- ✅ Disaster recovery to completely new infrastructure
- ❌ Less component-aware (treats as files)
- ❌ Slower for granular component restore
**With Two-Tier**:
- Local: 3.5TB (7 days)
- Kopia vault: ~500GB + (30 × 10GB) = ~800GB
- **Savings**: 70-80% in vault storage
### Storage Efficiency
### Component Restore Priority
Using this two-tier approach:
- **Local**: Immich creates ~7 days of native backups (may be large, but short retention)
- **Offsite**: Kopia deduplicates these backups for long-term vault storage (much smaller)
| Component | When to Restore | Can Regenerate? | Priority |
|-----------|----------------|-----------------|----------|
| Database | Always | No | Critical |
| Upload/Library | Always | No | Critical |
| Thumbnails | Optional | Yes | Medium |
| Encoded Videos | Optional | Yes | Low |
| Model Cache | Never | Yes | Low |
Example storage calculation (500GB library):
- Local: 7 days × 500GB = ~3.5TB (before compression)
- Kopia (offsite): First backup ~500GB, subsequent backups only store changes (might be <10GB/day after dedup)
### Compression Formats
Immich backups use `gzip` compression:
- **Database dumps**: `.sql.gz` files (typically 80-90% compression)
- **Library archives**: `.tar.gz` files (photos already compressed, ~40-60% reduction)
## Additional Resources
- [Immich Official Documentation](https://immich.app/docs)
- [Immich Official Backup Documentation](https://immich.app/docs/administration/backup-and-restore)
- [Kopia Documentation](https://kopia.io/docs/)
- [Docker Volume Backup Best Practices](https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes)
- [PostgreSQL pg_dump Documentation](https://www.postgresql.org/docs/current/app-pgdump.html)
## Revision History
| Date | Version | Changes |
|------|---------|---------|
| 2026-02-13 | 1.0 | Initial documentation - two-tier backup strategy |
| 2026-02-13 | 1.0 | Initial documentation - two-tier backup strategy using Immich's native backup method |
---
**Last Updated**: February 13, 2026
**Maintained By**: System Administrator
**Review Schedule**: Quarterly