Netgrimoire/immich_backup.md
2026-02-14 03:14:50 +00:00

7.2 KiB

title description published date tags editor dateCreated
Immich Backup and Restore Immich backup with Kopia true 2026-02-14T03:14:32.594Z markdown 2026-02-14T03:14:32.594Z

#!/bin/bash

Immich Backup Script

Two-tier approach matching mailcow backup strategy:

Tier 1 (Local): Component-level backups (database + volumes)

Tier 2 (Offsite): Kopia snapshots to vaults

set -euo pipefail

Configuration

BACKUP_DIR="/opt/immich-backups" RETENTION_DAYS=7 # Keep local backups for 7 days (like mailcow) BACKUP_DATE=$(date +%Y%m%d_%H%M%S) BACKUP_PATH="${BACKUP_DIR}/immich-${BACKUP_DATE}" LOG_FILE="/var/log/immich-backup.log"

Immich configuration (adjust these to your setup)

IMMICH_DIR="/opt/immich" # or wherever your docker-compose.yml is COMPOSE_FILE="${IMMICH_DIR}/docker-compose.yml" POSTGRES_CONTAINER="immich_postgres" # adjust if different PROJECT_NAME="immich" # Docker Compose project name

Kopia configuration

ENABLE_KOPIA=true # Set to false to disable offsite backups KOPIA_TAGS="immich,tier1-backup"

Colors for output

RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color

log() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" }

error() { echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR:${NC} $1" | tee -a "$LOG_FILE" }

warn() { echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING:${NC} $1" | tee -a "$LOG_FILE" }

Create backup directory

mkdir -p "${BACKUP_PATH}"

log "========================================" log "Starting Immich Tier 1 (Local) backup" log "Backup path: ${BACKUP_PATH}" log "========================================"

1. Backup PostgreSQL database

log "Backing up PostgreSQL database..." docker exec -t "${POSTGRES_CONTAINER}" pg_dumpall -c -U postgres | gzip > "${BACKUP_PATH}/database.sql.gz"

if [ $? -eq 0 ]; then DB_SIZE=$(du -h "${BACKUP_PATH}/database.sql.gz" | cut -f1) log "Database backup completed successfully (${DB_SIZE})" else error "Database backup failed" exit 1 fi

2. Backup Immich volumes/data

log "Backing up Immich data volumes..."

Get actual Docker volumes for this project

DOCKER_VOLUMES=$(docker volume ls --filter "name=${PROJECT_NAME}" --format "{{.Name}}")

if [ -z "${DOCKER_VOLUMES}" ]; then warn "No Docker volumes found with name pattern '${PROJECT_NAME}'. Checking for bind mounts..."

# Fallback to bind mounts if present
VOLUMES=(
    "upload"
    "library"
    "profile"
    "video"
    "thumbs"
    "encoded-video"
)

for volume in "${VOLUMES[@]}"; do
    SOURCE_PATH="${IMMICH_DIR}/${volume}"
    if [ -d "${SOURCE_PATH}" ]; then
        log "Backing up bind mount: ${volume}..."
        tar -czf "${BACKUP_PATH}/${volume}.tar.gz" -C "${IMMICH_DIR}" "${volume}" 2>/dev/null || warn "${volume} backup failed"
    fi
done

else # Backup Docker volumes for vol in ${DOCKER_VOLUMES}; do log "Backing up Docker volume: ${vol}" docker run --rm
-v "${vol}:/data:ro"
-v "${BACKUP_PATH}:/backup"
alpine tar -czf "/backup/${vol}.tar.gz" -C /data . || warn "Failed to backup ${vol}"

    if [ -f "${BACKUP_PATH}/${vol}.tar.gz" ]; then
        VOL_SIZE=$(du -h "${BACKUP_PATH}/${vol}.tar.gz" | cut -f1)
        log "  ✓ ${vol} backed up (${VOL_SIZE})"
    fi
done

fi

3. Backup configuration files

log "Backing up configuration files..." if [ -f "${COMPOSE_FILE}" ]; then cp "${COMPOSE_FILE}" "${BACKUP_PATH}/docker-compose.yml" log " ✓ docker-compose.yml" fi

if [ -f "${IMMICH_DIR}/.env" ]; then cp "${IMMICH_DIR}/.env" "${BACKUP_PATH}/.env" log " ✓ .env" fi

Backup any custom config files

if [ -d "${IMMICH_DIR}/config" ]; then tar -czf "${BACKUP_PATH}/config.tar.gz" -C "${IMMICH_DIR}" config 2>/dev/null && log " ✓ config directory" || true fi

4. Create backup manifest (matching mailcow style)

log "Creating backup manifest..." cat > "${BACKUP_PATH}/manifest.txt" << EOF Immich Backup Manifest Created: ${BACKUP_DATE} Hostname: $(hostname) Immich Directory: ${IMMICH_DIR} Project Name: ${PROJECT_NAME}

Backup Contents: $(ls -lh "${BACKUP_PATH}" 2>/dev/null)

Component Sizes: Database: $(du -h "${BACKUP_PATH}/database.sql.gz" 2>/dev/null | cut -f1 || echo "N/A") Total Backup Size: $(du -sh "${BACKUP_PATH}" 2>/dev/null | cut -f1)

Docker Volumes Backed Up: ${DOCKER_VOLUMES:-None (using bind mounts)} EOF

5. Calculate checksums

log "Calculating checksums..." cd "${BACKUP_PATH}" find . -type f -name ".tar.gz" -o -name ".sql.gz" | xargs sha256sum > checksums.sha256 2>/dev/null || warn "Checksum creation failed" cd - > /dev/null

TIER1_SIZE=$(du -sh "${BACKUP_PATH}" | cut -f1) log "Tier 1 (Local) backup completed! Size: ${TIER1_SIZE}"

6. Tier 2: Create Kopia snapshot for offsite backup

if [ "$ENABLE_KOPIA" = true ]; then log "========================================" log "Starting Tier 2 (Offsite) Kopia backup" log "========================================"

# Check if kopia is available
if ! command -v kopia &> /dev/null; then
    warn "Kopia not found. Skipping offsite backup."
    warn "Install kopia or set ENABLE_KOPIA=false"
else
    # Snapshot the backup directory
    log "Creating Kopia snapshot of backup directory..."
    kopia snapshot create "${BACKUP_DIR}" \
        --tags "${KOPIA_TAGS}" \
        --description "Immich backup ${BACKUP_DATE}" \
        2>&1 | tee -a "$LOG_FILE"
    
    KOPIA_EXIT=${PIPESTATUS[0]}
    
    if [ $KOPIA_EXIT -eq 0 ]; then
        log "Kopia snapshot completed successfully"
        
        # Also snapshot the Immich installation directory (configs)
        log "Creating Kopia snapshot of installation directory..."
        kopia snapshot create "${IMMICH_DIR}" \
            --tags "immich,config,docker-compose" \
            --description "Immich config ${BACKUP_DATE}" \
            2>&1 | tee -a "$LOG_FILE"
    else
        warn "Kopia snapshot failed with exit code ${KOPIA_EXIT}"
        warn "Local backup exists but offsite copy may be incomplete"
    fi
fi

fi

7. Cleanup old backups (retention policy)

log "========================================" log "Applying retention policy (${RETENTION_DAYS} days)" log "========================================"

Find and remove old local backups

REMOVED_COUNT=0 while IFS= read -r old_backup; do log "Removing old backup: $(basename "$old_backup")" rm -rf "$old_backup" ((REMOVED_COUNT++)) done < <(find "${BACKUP_DIR}" -maxdepth 1 -type d -name "immich-*" -mtime +${RETENTION_DAYS} 2>/dev/null)

if [ $REMOVED_COUNT -gt 0 ]; then log "Removed ${REMOVED_COUNT} old backup(s)" else log "No old backups to remove" fi

8. Final summary

log "========================================" log "Backup Summary" log "========================================" log "Tier 1 (Local): ${TIER1_SIZE} in ${BACKUP_PATH}" log "Tier 2 (Offsite): Kopia snapshot created" log "Retention: Keeping last ${RETENTION_DAYS} days locally" log "Log file: ${LOG_FILE}" log "========================================" log "Backup completed successfully!" log "========================================"

exit 0