docs: update immich_backup
This commit is contained in:
parent
7aa392c5b9
commit
8ffe3252a2
1 changed files with 133 additions and 51 deletions
184
immich_backup.md
184
immich_backup.md
|
|
@ -2,7 +2,7 @@
|
||||||
title: Immich Backup and Restore
|
title: Immich Backup and Restore
|
||||||
description: Immich backup with Kopia
|
description: Immich backup with Kopia
|
||||||
published: true
|
published: true
|
||||||
date: 2026-02-14T03:34:42.017Z
|
date: 2026-02-14T03:54:27.661Z
|
||||||
tags:
|
tags:
|
||||||
editor: markdown
|
editor: markdown
|
||||||
dateCreated: 2026-02-14T03:14:32.594Z
|
dateCreated: 2026-02-14T03:14:32.594Z
|
||||||
|
|
@ -108,11 +108,21 @@ Immich's official backup approach uses `pg_dump` for the database:
|
||||||
- Produces compressed `.sql.gz` files
|
- Produces compressed `.sql.gz` files
|
||||||
- Database remains available during backup
|
- Database remains available during backup
|
||||||
|
|
||||||
|
For the photo/video library, we use a **hybrid approach**:
|
||||||
|
- **Database**: Backed up locally as `dump.sql.gz` for fast component-level restore
|
||||||
|
- **Library**: Backed up directly by Kopia (no tar) for optimal deduplication and incremental backups
|
||||||
|
|
||||||
|
**Why not tar the library?**
|
||||||
|
- Kopia deduplicates at the file level - adding 1 photo shouldn't require backing up the entire library again
|
||||||
|
- Individual file access for selective restore
|
||||||
|
- Better compression and faster incremental backups
|
||||||
|
- Lower risk - corrupted tar loses everything, corrupted file only affects that file
|
||||||
|
|
||||||
**Key Features:**
|
**Key Features:**
|
||||||
- No downtime required
|
- No downtime required
|
||||||
- Consistent point-in-time snapshot
|
- Consistent point-in-time snapshot
|
||||||
- Standard PostgreSQL format (portable across systems)
|
- Standard PostgreSQL format (portable across systems)
|
||||||
- Can be restored to any PostgreSQL instance
|
- Efficient incremental backups of photo library
|
||||||
|
|
||||||
## Setting Up Immich Backups
|
## Setting Up Immich Backups
|
||||||
|
|
||||||
|
|
@ -150,17 +160,23 @@ docker exec -t immich_postgres pg_dump \
|
||||||
--username=postgres \
|
--username=postgres \
|
||||||
| gzip > "/opt/immich-backups/dump.sql.gz"
|
| gzip > "/opt/immich-backups/dump.sql.gz"
|
||||||
|
|
||||||
# Backup library (photos/videos)
|
|
||||||
tar -czf /opt/immich-backups/library.tar.gz -C /srv/immich library
|
|
||||||
|
|
||||||
# Backup configuration files
|
# Backup configuration files
|
||||||
cp docker-compose.yml /opt/immich-backups/
|
cp docker-compose.yml /opt/immich-backups/
|
||||||
cp .env /opt/immich-backups/
|
cp .env /opt/immich-backups/
|
||||||
|
|
||||||
|
# Backup library with Kopia (no tar - better deduplication)
|
||||||
|
kopia snapshot create /srv/immich/library \
|
||||||
|
--tags immich,library,photos \
|
||||||
|
--description "Immich library manual backup"
|
||||||
```
|
```
|
||||||
|
|
||||||
**What gets created:**
|
**What gets created:**
|
||||||
- Backup directory: `/opt/immich-backups/immich-YYYY-MM-DD-HH-MM-SS/`
|
- Local backup directory: `/opt/immich-backups/immich-YYYY-MM-DD-HH-MM-SS/`
|
||||||
- Contains: `dump.sql.gz` (database), `library.tar.gz` (photos), config files
|
- Contains: `dump.sql.gz` (database), config files
|
||||||
|
- Kopia snapshots:
|
||||||
|
- `/opt/immich-backups` (database + config)
|
||||||
|
- `/srv/immich/library` (photos/videos, no tar)
|
||||||
|
- `/opt/immich` (installation directory)
|
||||||
|
|
||||||
#### Step 3: Automated Backup Script
|
#### Step 3: Automated Backup Script
|
||||||
|
|
||||||
|
|
@ -213,18 +229,16 @@ fi
|
||||||
|
|
||||||
echo "[${BACKUP_DATE}] Immich database backup completed successfully" | tee -a "$LOG_FILE"
|
echo "[${BACKUP_DATE}] Immich database backup completed successfully" | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
# Step 2: Backup upload library (photos/videos)
|
# Step 2: Verify library location exists (Kopia will backup directly, no tar needed)
|
||||||
echo "[${BACKUP_DATE}] Backing up upload library..." | tee -a "$LOG_FILE"
|
echo "[${BACKUP_DATE}] Verifying library location..." | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
# Get the upload location from docker-compose volumes
|
# Get the upload location from docker-compose volumes
|
||||||
UPLOAD_LOCATION="/srv/immich/library"
|
UPLOAD_LOCATION="/srv/immich/library"
|
||||||
|
|
||||||
if [ -d "${UPLOAD_LOCATION}" ]; then
|
if [ -d "${UPLOAD_LOCATION}" ]; then
|
||||||
tar -czf "${BACKUP_DIR}/immich-${BACKUP_DATE}/library.tar.gz" \
|
LIBRARY_SIZE=$(du -sh ${UPLOAD_LOCATION} | cut -f1)
|
||||||
-C "$(dirname ${UPLOAD_LOCATION})" \
|
echo "[${BACKUP_DATE}] Library location verified: ${UPLOAD_LOCATION} (${LIBRARY_SIZE})" | tee -a "$LOG_FILE"
|
||||||
"$(basename ${UPLOAD_LOCATION})" 2>&1 | tee -a "$LOG_FILE"
|
echo "[${BACKUP_DATE}] Kopia will backup library files directly (no tar, better deduplication)" | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
echo "[${BACKUP_DATE}] Upload library backup completed" | tee -a "$LOG_FILE"
|
|
||||||
else
|
else
|
||||||
echo "[${BACKUP_DATE}] WARNING: Upload location not found at ${UPLOAD_LOCATION}" | tee -a "$LOG_FILE"
|
echo "[${BACKUP_DATE}] WARNING: Upload location not found at ${UPLOAD_LOCATION}" | tee -a "$LOG_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
@ -262,7 +276,25 @@ fi
|
||||||
|
|
||||||
echo "[${BACKUP_DATE}] Kopia snapshot completed successfully" | tee -a "$LOG_FILE"
|
echo "[${BACKUP_DATE}] Kopia snapshot completed successfully" | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
# Step 6: Also backup the Immich installation directory (configs, compose files)
|
# Step 6: Backup the library directly with Kopia (better deduplication than tar)
|
||||||
|
echo "[${BACKUP_DATE}] Creating Kopia snapshot of library..." | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
if [ -d "${UPLOAD_LOCATION}" ]; then
|
||||||
|
kopia snapshot create "${UPLOAD_LOCATION}" \
|
||||||
|
--tags immich,library,photos \
|
||||||
|
--description "Immich library ${BACKUP_DATE}" \
|
||||||
|
2>&1 | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
KOPIA_LIB_EXIT=${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
if [ $KOPIA_LIB_EXIT -ne 0 ]; then
|
||||||
|
echo "[${BACKUP_DATE}] WARNING: Kopia library snapshot failed" | tee -a "$LOG_FILE"
|
||||||
|
else
|
||||||
|
echo "[${BACKUP_DATE}] Library snapshot completed successfully" | tee -a "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 7: Also backup the Immich installation directory (configs, compose files)
|
||||||
echo "[${BACKUP_DATE}] Backing up Immich installation directory..." | tee -a "$LOG_FILE"
|
echo "[${BACKUP_DATE}] Backing up Immich installation directory..." | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
kopia snapshot create "${IMMICH_DIR}" \
|
kopia snapshot create "${IMMICH_DIR}" \
|
||||||
|
|
@ -322,15 +354,19 @@ docker compose down
|
||||||
# List available backups
|
# List available backups
|
||||||
ls -lh /opt/immich-backups/
|
ls -lh /opt/immich-backups/
|
||||||
|
|
||||||
# Choose a backup
|
# Choose a database backup
|
||||||
BACKUP_PATH="/opt/immich-backups/immich-YYYYMMDD_HHMMSS"
|
BACKUP_PATH="/opt/immich-backups/immich-YYYYMMDD_HHMMSS"
|
||||||
|
|
||||||
# Restore database
|
# Restore database
|
||||||
gunzip < ${BACKUP_PATH}/dump.sql.gz | \
|
gunzip < ${BACKUP_PATH}/dump.sql.gz | \
|
||||||
docker compose exec -T database psql --username=postgres --dbname=immich
|
docker compose exec -T database psql --username=postgres --dbname=immich
|
||||||
|
|
||||||
# Restore library
|
# Restore library from Kopia
|
||||||
tar -xzf ${BACKUP_PATH}/library.tar.gz -C /srv/immich/
|
kopia snapshot list --tags library
|
||||||
|
kopia restore <library-snapshot-id> /srv/immich/library
|
||||||
|
|
||||||
|
# Fix permissions
|
||||||
|
chown -R 1000:1000 /srv/immich/library
|
||||||
|
|
||||||
# Restore configuration (review changes first)
|
# Restore configuration (review changes first)
|
||||||
cp ${BACKUP_PATH}/.env .env.restored
|
cp ${BACKUP_PATH}/.env .env.restored
|
||||||
|
|
@ -376,19 +412,17 @@ cd /opt/immich
|
||||||
# Stop Immich
|
# Stop Immich
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
# Restore library from backup
|
# Restore library from Kopia
|
||||||
BACKUP_PATH="/opt/immich-backups/immich-YYYYMMDD_HHMMSS"
|
kopia snapshot list --tags library
|
||||||
tar -xzf ${BACKUP_PATH}/library.tar.gz -C /srv/immich/
|
kopia restore <library-snapshot-id> /srv/immich/library
|
||||||
|
|
||||||
|
# Fix permissions
|
||||||
|
chown -R 1000:1000 /srv/immich/library
|
||||||
|
|
||||||
# Start Immich
|
# Start Immich
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note**: For library restore, ensure proper permissions after extraction:
|
|
||||||
```bash
|
|
||||||
chown -R 1000:1000 /srv/immich/library
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 2: Complete Server Rebuild (Kopia Restore)
|
### Method 2: Complete Server Rebuild (Kopia Restore)
|
||||||
|
|
||||||
Use this when recovering to a completely new server or when local backups are unavailable.
|
Use this when recovering to a completely new server or when local backups are unavailable.
|
||||||
|
|
@ -477,8 +511,9 @@ sleep 30
|
||||||
gunzip < ${LATEST_BACKUP}/dump.sql.gz | \
|
gunzip < ${LATEST_BACKUP}/dump.sql.gz | \
|
||||||
docker compose exec -T database psql --username=postgres --dbname=immich
|
docker compose exec -T database psql --username=postgres --dbname=immich
|
||||||
|
|
||||||
# Restore library
|
# Restore library from Kopia
|
||||||
tar -xzf ${LATEST_BACKUP}/library.tar.gz -C /srv/immich/
|
kopia snapshot list --tags library
|
||||||
|
kopia restore <library-snapshot-id> /srv/immich/library
|
||||||
|
|
||||||
# Fix permissions
|
# Fix permissions
|
||||||
chown -R 1000:1000 /srv/immich/library
|
chown -R 1000:1000 /srv/immich/library
|
||||||
|
|
@ -519,35 +554,52 @@ ls -lah /srv/immich/library/
|
||||||
|
|
||||||
To restore a single user's library without affecting others:
|
To restore a single user's library without affecting others:
|
||||||
|
|
||||||
|
**Option A: Using Kopia Mount (Recommended)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/immich
|
# Mount the Kopia snapshot
|
||||||
|
kopia snapshot list --tags library
|
||||||
# Stop Immich temporarily
|
mkdir -p /mnt/kopia-library
|
||||||
docker compose down
|
kopia mount <library-snapshot-id> /mnt/kopia-library &
|
||||||
|
|
||||||
# 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)
|
# Find the user's directory (using user ID from database)
|
||||||
# User libraries are typically in: library/{user-uuid}/
|
# User libraries are typically in: library/{user-uuid}/
|
||||||
USER_UUID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
USER_UUID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||||
|
|
||||||
# Copy user's data back
|
# Copy user's data back
|
||||||
rsync -av /tmp/immich-restore/library/${USER_UUID}/ \
|
rsync -av /mnt/kopia-library/${USER_UUID}/ \
|
||||||
/srv/immich/library/${USER_UUID}/
|
/srv/immich/library/${USER_UUID}/
|
||||||
|
|
||||||
# Fix permissions
|
# Fix permissions
|
||||||
chown -R 1000:1000 /srv/immich/library/${USER_UUID}/
|
chown -R 1000:1000 /srv/immich/library/${USER_UUID}/
|
||||||
|
|
||||||
# Cleanup
|
# Unmount
|
||||||
rm -rf /tmp/immich-restore
|
kopia unmount /mnt/kopia-library
|
||||||
|
|
||||||
|
# Restart Immich to recognize changes
|
||||||
|
cd /opt/immich
|
||||||
|
docker compose restart immich-server
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Selective Kopia Restore**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/immich
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Restore just the specific user's directory
|
||||||
|
kopia snapshot list --tags library
|
||||||
|
USER_UUID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||||
|
|
||||||
|
# Restore with path filter
|
||||||
|
kopia restore <library-snapshot-id> /srv/immich/library \
|
||||||
|
--snapshot-path="${USER_UUID}"
|
||||||
|
|
||||||
|
# Fix permissions
|
||||||
|
chown -R 1000:1000 /srv/immich/library/${USER_UUID}/
|
||||||
|
|
||||||
# Start Immich
|
# Start Immich
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
# Trigger re-scan for this user via the web UI
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scenario 3: Database Recovery Only
|
### Scenario 3: Database Recovery Only
|
||||||
|
|
@ -726,18 +778,48 @@ When disaster strikes, follow this checklist:
|
||||||
### Storage Efficiency
|
### Storage Efficiency
|
||||||
|
|
||||||
Using this two-tier approach:
|
Using this two-tier approach:
|
||||||
- **Local**: Immich creates ~7 days of native backups (may be large, but short retention)
|
- **Local**: Database backups (~7 days retention, relatively small)
|
||||||
- **Offsite**: Kopia deduplicates these backups for long-term vault storage (much smaller)
|
- **Kopia**: Database backups + library (efficient deduplication)
|
||||||
|
|
||||||
Example storage calculation (500GB library):
|
**Why library goes directly to Kopia without tar:**
|
||||||
- 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
|
Example with 500GB library, adding 10GB photos/month:
|
||||||
|
|
||||||
Immich backups use `gzip` compression:
|
**With tar approach:**
|
||||||
- **Database dumps**: `.sql.gz` files (typically 80-90% compression)
|
- Month 1: Backup 500GB tar
|
||||||
- **Library archives**: `.tar.gz` files (photos already compressed, ~40-60% reduction)
|
- Month 2: Add 10GB photos → Entire 510GB tar changes → Backup 510GB
|
||||||
|
- Month 3: Add 10GB photos → Entire 520GB tar changes → Backup 520GB
|
||||||
|
- **Total storage needed**: 500 + 510 + 520 = 1,530GB
|
||||||
|
|
||||||
|
**Without tar (Kopia direct):**
|
||||||
|
- Month 1: Backup 500GB
|
||||||
|
- Month 2: Add 10GB photos → Kopia only backs up the 10GB new files
|
||||||
|
- Month 3: Add 10GB photos → Kopia only backs up the 10GB new files
|
||||||
|
- **Total storage needed**: 500 + 10 + 10 = 520GB
|
||||||
|
|
||||||
|
**Savings**: ~66% reduction in storage and backup time!
|
||||||
|
|
||||||
|
This is why we:
|
||||||
|
- Keep database dumps local (small, fast component restore)
|
||||||
|
- Let Kopia handle library directly (efficient, incremental, deduplicated)
|
||||||
|
|
||||||
|
### Compression and Deduplication
|
||||||
|
|
||||||
|
**Database backups** use `gzip` compression:
|
||||||
|
- Typically 80-90% compression ratio for SQL dumps
|
||||||
|
- Small enough to keep local copies
|
||||||
|
|
||||||
|
**Library backups** use Kopia's built-in compression and deduplication:
|
||||||
|
- Photos (JPEG/HEIC): Already compressed, Kopia skips re-compression
|
||||||
|
- Videos: Already compressed, minimal additional compression
|
||||||
|
- RAW files: Some compression possible
|
||||||
|
- **Deduplication**: If you upload the same photo twice, Kopia stores it once
|
||||||
|
- **Block-level dedup**: Even modified photos share unchanged blocks
|
||||||
|
|
||||||
|
This is far more efficient than tar + gzip, which would:
|
||||||
|
- Compress already-compressed photos (wasted CPU, minimal benefit)
|
||||||
|
- Store entire archive even if only 1 file changed
|
||||||
|
- Prevent deduplication across backups
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue