Googleフォト代替の最新版「immich v3.0.1」対応セットアップスクリプト

Googleフォトの代替として使えると人気の「immich」。デモサイトも用意されているので、そちらを見るのが分かりやすいでしょう。さて、そのimmichは精力的にバージョンアップを繰り返しており、現在はv3系となっています(執筆時点の最新バージョンはv3.0.1 GitHub)。変わった点はこのあたりのようです。

さて、これまでも普段からimmichを活用しており、予備としてクラウドへのバックアップも行っていたのですけれど、昨日より、そのクラウドへのバックアップにVM内のWIndowsを使うのをやめました。
この機会にと、immichのセットアップスクリプトも見直してみました。

写真データをバックアップしやすいように、データベースやサムネイルなどはコンテナ内に格納し、肝心の写真データ原本のみホストからも見える環境にしている/opt/lxd-data以下に保存するようにしています。具体的には /opt/lxd-data/immich-library 直下が admin/ になります。
このフォルダを「EasyRclone」や「rsyncGUI」でバックアップすればよいかと。

LXDコンテナにインストール

#!/bin/bash
set -euo pipefail
# =============================================================
#  Immich セットアップスクリプト (tailscale serve版)
#
#  構成 (LXDコンテナ内で実行):
#   - Immich : https://<hostname>.<tailnet>.ts.net:3307 (tailscale serve 3307)
#
#  ディレクトリ構成:
#   /opt/docker/immich/
#     docker-compose.yml
#     .env                 ← DBパスワード等の永続化
#     postgres/            ← PostgreSQLデータ
#     upload/               ← thumbs/upload/profile/backups/encoded-video等
#   /opt/lxd-data/immich-library/   ← ユーザーごとの写真本体(例: admin/)
#
#  前提条件:
#   - LXDコンテナ内でrootまたはsudoで実行
#   - Docker がインストール済みであること
#   - tailscale up 済みであること
#   - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
#     https://login.tailscale.com/admin/dns
# =============================================================

IMMICH_DIR="/opt/docker/immich"
LIBRARY_DIR="/opt/lxd-data/immich-library"   # ユーザー写真データのみ(/library配下を直接ここに)
UPLOAD_SUBDIR="${IMMICH_DIR}/upload"          # thumbs/upload/profile/backups等の残り
IMMICH_PORT=2283         # ホスト内部ポート(127.0.0.1バインド・元のまま)
TAILSCALE_PORT=3307      # tailscale serveで公開するポート

# ── カラー出力 ────────────────────────────────────
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  Immich セットアップ (tailscale serve版)"
echo "════════════════════════════════════════"
echo ""

# ── Tailscale確認 ─────────────────────────────────
if ! command -v tailscale &>/dev/null; then
    echo -e "${RED}ERROR: tailscaleがインストールされていません${NC}"
    exit 1
fi

if ! tailscale status &>/dev/null 2>&1; then
    echo -e "${RED}ERROR: tailscaleが接続されていません。tailscale up を実行してください${NC}"
    exit 1
fi

# ── tailnetドメイン取得 ───────────────────────────
echo "==> [1/5] Tailscaleドメインを取得..."
sudo tailscale set --operator=$USER 2>/dev/null || true

TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('Self', {}).get('DNSName', '').rstrip('.'))
" 2>/dev/null)

if [ -z "$TAILSCALE_DOMAIN" ]; then
    echo -e "${RED}ERROR: Tailscaleドメインを取得できませんでした${NC}"
    echo "Tailscale管理コンソールでMagicDNSが有効になっているか確認してください"
    exit 1
fi

echo -e "  ${GREEN}ドメイン: ${TAILSCALE_DOMAIN}${NC}"
echo -e "  ${GREEN}Immich : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}${NC}"

# ── ディレクトリ作成 ──────────────────────────────
echo ""
echo "==> [2/5] ディレクトリを準備..."
mkdir -p "${IMMICH_DIR}/postgres"
mkdir -p "${UPLOAD_SUBDIR}"
mkdir -p "${LIBRARY_DIR}"
echo -e "  ${GREEN}✓ ${IMMICH_DIR}/postgres${NC}"
echo -e "  ${GREEN}✓ ${UPLOAD_SUBDIR}${NC}"
echo -e "  ${GREEN}✓ ${LIBRARY_DIR}(写真本体)${NC}"

# ── .env 生成 or 既存を使用 ───────────────────────
echo ""
echo "==> [3/5] .env を確認..."

if [ -f "${IMMICH_DIR}/.env" ]; then
    source "${IMMICH_DIR}/.env"
    echo -e "  ${GREEN}✓ 既存の .env を使用${NC}"
else
    if [ "$(find "${IMMICH_DIR}/postgres" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l)" -gt 0 ]; then
        echo -e "${RED}ERROR: DBデータが存在しますが .env が見つかりません${NC}"
        echo -e "${RED}       ${IMMICH_DIR}/.env が必要です${NC}"
        echo ""
        echo "  対処法: DBデータを削除して再セットアップ:"
        echo "    docker compose -f ${IMMICH_DIR}/docker-compose.yml down -v"
        echo "    rm -rf ${IMMICH_DIR}/postgres/*"
        exit 1
    fi

    DB_PASSWORD=$(openssl rand -hex 16)

    cat > "${IMMICH_DIR}/.env" <<EOF
UPLOAD_LOCATION=${UPLOAD_SUBDIR}
LIBRARY_LOCATION=${LIBRARY_DIR}
DB_DATA_LOCATION=${IMMICH_DIR}/postgres
IMMICH_VERSION=release
DB_PASSWORD=${DB_PASSWORD}
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
EOF
    chmod 600 "${IMMICH_DIR}/.env"
    echo -e "  ${GREEN}✓ 新しい .env を生成${NC}"
fi

# ── docker-compose.yml 生成 ───────────────────────
echo ""
echo "==> [4/5] 設定ファイルを生成..."

cat > "${IMMICH_DIR}/docker-compose.yml" <<'EOF'
name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    volumes:
      - ${UPLOAD_LOCATION}:/data
      - ${LIBRARY_LOCATION}:/data/library
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '127.0.0.1:2283:2283'
    depends_on:
      - redis
      - database
    restart: always
    healthcheck:
      disable: false

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: always
    healthcheck:
      disable: false

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:9
    healthcheck:
      test: redis-cli ping || exit 1
    restart: always

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    shm_size: 128mb
    healthcheck:
      disable: false
    restart: always

volumes:
  model-cache:
EOF

echo -e "  ${GREEN}✓ ${IMMICH_DIR}/docker-compose.yml${NC}"

# ── Docker起動 & tailscale serve設定 ─────────────
echo ""
echo "==> [5/5] コンテナを起動..."

cd "${IMMICH_DIR}"
docker compose pull
docker compose up -d

echo "  ⏳ Immich serverの起動を待機中(最大180秒)..."
for i in $(seq 1 36); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${IMMICH_PORT}/" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" != "000" ]; then
        echo -e "\n  ${GREEN}✓ Immich server 起動完了 (HTTP ${HTTP_CODE})${NC}"
        break
    fi
    if [ "$i" -eq 36 ]; then
        echo -e "\n${RED}ERROR: Immich serverがタイムアウトしました${NC}"
        docker logs immich_server --tail=20
        exit 1
    fi
    sleep 5
    echo -n "."
done

# 既存のserve設定を残しつつImmichのポートのみ追加(冪等対応)
tailscale serve --https=${TAILSCALE_PORT} off 2>/dev/null || true
tailscale serve --bg --https=${TAILSCALE_PORT} http://localhost:${IMMICH_PORT}
echo -e "  ${GREEN}✓ tailscale serve 設定完了${NC}"

echo ""
tailscale serve status

# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 URL         : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}"
echo "  🗄️  DBパスワード : ${DB_PASSWORD}"
echo ""
echo "  📁 ディレクトリ構成:"
echo "    設定    : ${IMMICH_DIR}/.env"
echo "    写真    : ${LIBRARY_DIR}/(ユーザー名フォルダ直下)"
echo "    その他  : ${UPLOAD_SUBDIR}/(thumbs, upload, profile, backups等)"
echo "    DB      : ${IMMICH_DIR}/postgres/"
echo ""
echo "════════════════════════════════════════"
echo "  🔧 アップデート手順"
echo "════════════════════════════════════════"
echo ""
echo "  cd ${IMMICH_DIR} && docker compose pull && docker compose up -d"
echo ""
echo "════════════════════════════════════════"
echo ""

テンプレート

これは個人的に利用しているテンプレートです。

{{y}}/{{y}}{{MM}}/{{y}}{{MM}}{{dd}}_{{album}}/{{filename}}

アップデート手順

immich自体のアップデート手順は以前と変わりません。immichのあるコンテナに入ったら下記を実行します。

cd /opt/docker/immich && docker compose pull && docker compose up -d

バックアップスクリプト(immich-backup.sh)

別の環境に移行する場合などのフルバックアップです。今回のインストールスクリプトに対応しているはずです。
ただ、今回のスクリプトはまだ実際には試していないのであくまでも自己責任で
まずバックアップスクリプトを作成してバックアップを実行します。LXDコンテナで動かしている場合は、LXDコンテナ内に入って作業します。

mkdir -p /opt/lxd-data/immich
cd /opt/lxd-data/immich
nano immich-backup.sh
chmod +x immich-backup.sh
#!/bin/bash
set -euo pipefail
# =============================================================
#  Immich バックアップスクリプト(ライブラリ分離構成版)
#
#  対象環境:
#   - Immich : https://<hostname>.<tailnet>.ts.net:3307 (tailscale serve 3307)
#   - データ : /opt/docker/immich/
#     UPLOAD_LOCATION  = /opt/docker/immich/upload      (thumbs/upload/profile/backups/encoded-video)
#     LIBRARY_LOCATION = /opt/lxd-data/immich-library    (admin/ = 写真本体, .immich)
#
#  使い方:
#   ./immich-backup.sh [バックアップ先ディレクトリ]
#
#  例:
#   ./immich-backup.sh                        # デフォルト: /opt/lxd-data/immich-backup
#   ./immich-backup.sh /mnt/usbssd/immich     # 外付けSSD等を指定
#
#  バックアップ先:
#   <指定ディレクトリ>/
#     backup-<TIMESTAMP>/
#       postgres.dump       ← PostgreSQL全体ダンプ (pg_dumpall)
#       upload.tar.gz       ← thumbs/upload/profile/backups/encoded-video
#       library.tar.gz      ← 写真本体(admin/等ユーザーフォルダ + .immich)
#       .env                ← 環境変数(DBパスワード等)
#       docker-compose.yml  ← ネストマウント構成の記録
#       BACKUP_INFO         ← バックアップメタ情報
#
#  実行場所: Immichが動作しているLXDコンテナ内
# =============================================================

IMMICH_DIR="/opt/docker/immich"
DEFAULT_BACKUP_BASE="/opt/lxd-data/immich-backup"
BACKUP_BASE="${1:-${DEFAULT_BACKUP_BASE}}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="${BACKUP_BASE}/backup-${TIMESTAMP}"

# ── カラー出力 ─────────────────────────────────────
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  Immich バックアップ(ライブラリ分離構成)"
echo "════════════════════════════════════════"
echo ""

if [ -z "${1:-}" ]; then
    echo -e "  バックアップ先 : ${BACKUP_DIR}"
    echo -e "  ${YELLOW}(デフォルト。変更する場合: $0 /mnt/usbssd/immich)${NC}"
else
    echo -e "  バックアップ先 : ${BACKUP_DIR}"
    echo -e "  ${GREEN}(引数で指定)${NC}"
fi
echo ""

# ── 前提確認 ──────────────────────────────────────
if [ ! -f "${IMMICH_DIR}/.env" ]; then
    echo -e "${RED}ERROR: ${IMMICH_DIR}/.env が見つかりません${NC}"
    exit 1
fi

if ! docker ps --format '{{.Names}}' | grep -q "^immich_postgres$"; then
    echo -e "${RED}ERROR: immich_postgres コンテナが起動していません${NC}"
    echo "  docker compose -f ${IMMICH_DIR}/docker-compose.yml up -d を実行してください"
    exit 1
fi

# .env から情報を読み込む
source "${IMMICH_DIR}/.env"

if [ -z "${LIBRARY_LOCATION:-}" ]; then
    echo -e "${RED}ERROR: .env に LIBRARY_LOCATION が設定されていません${NC}"
    echo "  このスクリプトはライブラリ分離構成(admin直下)専用です"
    exit 1
fi

echo -e "  DB名           : ${DB_DATABASE_NAME}"
echo -e "  UPLOAD_LOCATION  : ${UPLOAD_LOCATION}"
echo -e "  LIBRARY_LOCATION : ${LIBRARY_LOCATION}"
echo ""

# ── バックアップディレクトリ作成 ──────────────────
echo "==> [1/5] バックアップディレクトリを作成..."
mkdir -p "${BACKUP_DIR}"
echo -e "  ${GREEN}✓ ${BACKUP_DIR}${NC}"

# ── PostgreSQL ダンプ ─────────────────────────────
echo ""
echo "==> [2/5] PostgreSQLをダンプ中..."

echo -e "  ${YELLOW}⚠ immich_server を一時停止します(写真のアップロードが一時的に不可になります)${NC}"
docker stop immich_server immich-machine-learning 2>/dev/null || \
docker stop immich_server 2>/dev/null || true
docker ps --format '{{.Names}}' | grep -E "immich.*(machine|ml)" | xargs -r docker stop || true

docker exec immich_postgres pg_dumpall \
    -U "${DB_USERNAME}" \
    > "${BACKUP_DIR}/postgres.dump"

echo -e "  ${GREEN}✓ postgres.dump ($(du -sh "${BACKUP_DIR}/postgres.dump" | cut -f1))${NC}"

# ── upload/ をアーカイブ(thumbs, upload, profile, backups, encoded-video)
echo ""
echo "==> [3/5] upload(thumbs/upload/profile等)をアーカイブ中..."
tar -czf "${BACKUP_DIR}/upload.tar.gz" \
    -C "$(dirname "${UPLOAD_LOCATION}")" \
    "$(basename "${UPLOAD_LOCATION}")"
echo -e "  ${GREEN}✓ upload.tar.gz ($(du -sh "${BACKUP_DIR}/upload.tar.gz" | cut -f1))${NC}"

# ── library/ をアーカイブ(写真本体: admin/ + .immich)
echo ""
echo "==> [4/5] library(写真本体)をアーカイブ中..."
echo -e "  ${YELLOW}(サイズによっては時間がかかります)${NC}"
tar -czf "${BACKUP_DIR}/library.tar.gz" \
    -C "$(dirname "${LIBRARY_LOCATION}")" \
    "$(basename "${LIBRARY_LOCATION}")"
echo -e "  ${GREEN}✓ library.tar.gz ($(du -sh "${BACKUP_DIR}/library.tar.gz" | cut -f1))${NC}"

# immich_server を再起動
docker start immich_server 2>/dev/null || true
docker start immich-machine-learning 2>/dev/null || true
echo -e "  ${GREEN}✓ immich_server を再起動しました${NC}"

# ── 設定ファイルをコピー ──────────────────────────
echo ""
echo "==> [5/5] 設定ファイルをコピー..."
cp "${IMMICH_DIR}/.env" "${BACKUP_DIR}/.env"
chmod 600 "${BACKUP_DIR}/.env"
cp "${IMMICH_DIR}/docker-compose.yml" "${BACKUP_DIR}/docker-compose.yml"
echo -e "  ${GREEN}✓ .env${NC}"
echo -e "  ${GREEN}✓ docker-compose.yml${NC}"

# ── メタ情報を記録 ────────────────────────────────
IMMICH_VERSION_RUNNING=$(docker inspect immich_server \
    --format '{{index .Config.Image}}' 2>/dev/null || echo "unknown")

cat > "${BACKUP_DIR}/BACKUP_INFO" <<EOF
BACKUP_TIMESTAMP=${TIMESTAMP}
BACKUP_DATE=$(date '+%Y-%m-%d %H:%M:%S %Z')
SOURCE_DIR=${IMMICH_DIR}
IMMICH_IMAGE=${IMMICH_VERSION_RUNNING}
DB_USERNAME=${DB_USERNAME}
DB_DATABASE_NAME=${DB_DATABASE_NAME}
UPLOAD_LOCATION=${UPLOAD_LOCATION}
LIBRARY_LOCATION=${LIBRARY_LOCATION}
POSTGRES_DUMP_SIZE=$(du -sh "${BACKUP_DIR}/postgres.dump" | cut -f1)
UPLOAD_ARCHIVE_SIZE=$(du -sh "${BACKUP_DIR}/upload.tar.gz" | cut -f1)
LIBRARY_ARCHIVE_SIZE=$(du -sh "${BACKUP_DIR}/library.tar.gz" | cut -f1)
EOF

echo -e "  ${GREEN}✓ BACKUP_INFO${NC}"

# ── 完了 ──────────────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  バックアップ完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  📁 バックアップ先 : ${BACKUP_DIR}"
echo "  📊 合計サイズ     : $(du -sh "${BACKUP_DIR}" | cut -f1)"
echo ""
echo "  ファイル一覧:"
ls -lh "${BACKUP_DIR}"
echo ""
echo "  次のステップ:"
echo "    復元先の新環境で immich-restore.sh を実行してください"
echo "    バックアップディレクトリ: ${BACKUP_DIR}"
echo ""
echo "════════════════════════════════════════"
echo ""

/opt/lxd-data/immichに保存

デフォルトの場所/opt/lxd-data/immichに保存する場合。

sudo ./immich-backup.sh

外付けHDDなどに保存

外付けHDDなどを指定する場合は、LXDコンテナに追加します。
たとえばホスト接続したデバイスのマウント先が/run/media/user/128だった場合、下記のように指定します(ここではコンテナ内は/opt/ssdにマウントしています。

そして、コンテナに入り、バックアップ先を指定してスクリプトを実行します。

cd /opt/lxd-data/immich
sudo ./immich-backup.sh /opt/ssd

復元スクリプト(immich-restore.sh)

移行先のコンテナでImmichをセットアップします。
セットアップが完了したら、そのコンテナの中で下記を実行してスクリプトを保存します。

mkdir -p /opt/lxd-data/immich
cd /opt/lxd-data/immich
nano immich-restore.sh
chmod +x immich-restore.sh
#!/bin/bash
set -euo pipefail
# =============================================================
#  Immich 復元スクリプト(ライブラリ分離構成版)
#
#  復元先環境:
#   - Immich : https://<hostname>.<tailnet>.ts.net:3307 (tailscale serve 3307)
#   - データ : /opt/docker/immich/
#     UPLOAD_LOCATION  = /opt/docker/immich/upload
#     LIBRARY_LOCATION = /opt/lxd-data/immich-library
#
#  使い方:
#   ./immich-restore.sh /opt/lxd-data/immich-backup/backup-<TIMESTAMP>
#
#  実行タイミング:
#   セットアップスクリプト完了後(コンテナ起動済みの状態)に実行
#
#  注意:
#   復元先のDBとライブラリは上書きされます
# =============================================================

IMMICH_DIR="/opt/docker/immich"
BACKUP_DIR="${1:-}"

# ── カラー出力 ─────────────────────────────────────
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  Immich 復元(ライブラリ分離構成)"
echo "════════════════════════════════════════"
echo ""

# ── 引数チェック ──────────────────────────────────
if [ -z "${BACKUP_DIR}" ]; then
    echo -e "${RED}ERROR: バックアップディレクトリを引数で指定してください${NC}"
    echo ""
    echo "  使い方:"
    echo "    $0 /opt/lxd-data/immich-backup/backup-<TIMESTAMP>"
    echo ""
    echo "  利用可能なバックアップ:"
    ls -1d /opt/lxd-data/immich-backup/backup-* 2>/dev/null || echo "    (バックアップが見つかりません)"
    echo ""
    exit 1
fi

if [ ! -d "${BACKUP_DIR}" ]; then
    echo -e "${RED}ERROR: バックアップディレクトリが存在しません: ${BACKUP_DIR}${NC}"
    exit 1
fi

# ── バックアップファイルの存在確認 ───────────────
for f in postgres.dump upload.tar.gz library.tar.gz .env; do
    if [ ! -f "${BACKUP_DIR}/${f}" ]; then
        echo -e "${RED}ERROR: バックアップファイルが見つかりません: ${BACKUP_DIR}/${f}${NC}"
        echo "  旧形式(library.tar.gz単体)のバックアップの場合は復元スクリプトが異なります"
        exit 1
    fi
done

# ── バックアップ情報を表示 ────────────────────────
if [ -f "${BACKUP_DIR}/BACKUP_INFO" ]; then
    echo "  📋 バックアップ情報:"
    while IFS='=' read -r key val; do
        echo "    ${key} = ${val}"
    done < "${BACKUP_DIR}/BACKUP_INFO"
    echo ""
fi

# ── 復元先の確認 ──────────────────────────────────
if [ ! -f "${IMMICH_DIR}/.env" ]; then
    echo -e "${RED}ERROR: ${IMMICH_DIR}/.env が見つかりません${NC}"
    echo "  先にセットアップスクリプトを実行してください"
    exit 1
fi

if ! docker ps --format '{{.Names}}' | grep -q "^immich_postgres$"; then
    echo -e "${RED}ERROR: immich_postgres コンテナが起動していません${NC}"
    echo "  docker compose -f ${IMMICH_DIR}/docker-compose.yml up -d を実行してください"
    exit 1
fi

# 復元先の.envからDB情報を読み込む(新環境のDB認証情報を使う)
source "${IMMICH_DIR}/.env"

if [ -z "${LIBRARY_LOCATION:-}" ]; then
    echo -e "${RED}ERROR: 復元先の .env に LIBRARY_LOCATION が設定されていません${NC}"
    echo "  復元先はライブラリ分離構成(admin直下)である必要があります"
    exit 1
fi

echo "  復元元バックアップ   : ${BACKUP_DIR}"
echo "  復元先               : ${IMMICH_DIR}"
echo "  復元先DB名           : ${DB_DATABASE_NAME}"
echo "  復元先UPLOAD_LOCATION  : ${UPLOAD_LOCATION}"
echo "  復元先LIBRARY_LOCATION : ${LIBRARY_LOCATION}"
echo ""
echo -e "${YELLOW}⚠ 警告: 復元先の既存データはすべて上書きされます${NC}"
echo ""
read -rp "続行しますか? [y/N]: " CONFIRM
if [[ ! "${CONFIRM}" =~ ^[Yy]$ ]]; then
    echo "キャンセルしました"
    exit 0
fi
echo ""

# ── immich_server を停止 ──────────────────────────
echo "==> [1/5] Immichアプリコンテナを停止..."
docker stop immich_server 2>/dev/null || true
docker stop immich-machine-learning 2>/dev/null || true
docker ps --format '{{.Names}}' | grep -E "immich.*(machine|ml)" | xargs -r docker stop || true
echo -e "  ${GREEN}✓ 停止完了${NC}"

# ── PostgreSQLを復元 ─────────────────────────────
echo ""
echo "==> [2/5] PostgreSQLを復元中..."

echo "  既存データベースをリセット中..."
docker exec immich_postgres psql \
    -U "${DB_USERNAME}" \
    -d postgres \
    -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${DB_DATABASE_NAME}' AND pid <> pg_backend_pid();" \
    > /dev/null 2>&1 || true

docker exec immich_postgres psql \
    -U "${DB_USERNAME}" \
    -d postgres \
    -c "DROP DATABASE IF EXISTS ${DB_DATABASE_NAME};" \
    > /dev/null 2>&1

docker exec immich_postgres psql \
    -U "${DB_USERNAME}" \
    -d postgres \
    -c "DROP ROLE IF EXISTS ${DB_USERNAME};" \
    > /dev/null 2>&1 || true

echo "  pg_dumpallデータを復元中(時間がかかる場合があります)..."
docker exec -i immich_postgres psql \
    -U "${DB_USERNAME}" \
    -d postgres \
    < "${BACKUP_DIR}/postgres.dump"

echo "  DBパスワードを新環境の .env に合わせて更新中..."
docker exec immich_postgres psql \
    -U "${DB_USERNAME}" \
    -d postgres \
    -c "ALTER USER ${DB_USERNAME} WITH PASSWORD '${DB_PASSWORD}';" \
    > /dev/null 2>&1
echo -e "  ${GREEN}✓ PostgreSQL復元完了${NC}"

# ── upload/ を復元 ────────────────────────────────
echo ""
echo "==> [3/5] upload(thumbs/upload/profile等)を復元中..."

UPLOAD_PARENT=$(dirname "${UPLOAD_LOCATION}")
if [ -d "${UPLOAD_LOCATION}" ] && [ "$(ls -A "${UPLOAD_LOCATION}" 2>/dev/null)" ]; then
    ARCHIVE_NAME="${UPLOAD_LOCATION}.old.$(date +%Y%m%d_%H%M%S)"
    echo -e "  ${YELLOW}既存uploadを退避: ${ARCHIVE_NAME}${NC}"
    mv "${UPLOAD_LOCATION}" "${ARCHIVE_NAME}"
fi
mkdir -p "${UPLOAD_LOCATION}"
tar -xzf "${BACKUP_DIR}/upload.tar.gz" -C "${UPLOAD_PARENT}"
echo -e "  ${GREEN}✓ upload復元完了${NC}"

# ── library/ を復元 ───────────────────────────────
echo ""
echo "==> [4/5] library(写真本体)を復元中..."
echo -e "  ${YELLOW}(サイズによっては時間がかかります)${NC}"

LIBRARY_PARENT=$(dirname "${LIBRARY_LOCATION}")
if [ -d "${LIBRARY_LOCATION}" ] && [ "$(ls -A "${LIBRARY_LOCATION}" 2>/dev/null)" ]; then
    ARCHIVE_NAME="${LIBRARY_LOCATION}.old.$(date +%Y%m%d_%H%M%S)"
    echo -e "  ${YELLOW}既存libraryを退避: ${ARCHIVE_NAME}${NC}"
    mv "${LIBRARY_LOCATION}" "${ARCHIVE_NAME}"
fi
mkdir -p "${LIBRARY_LOCATION}"
tar -xzf "${BACKUP_DIR}/library.tar.gz" -C "${LIBRARY_PARENT}"
echo -e "  ${GREEN}✓ library復元完了${NC}"

# 整合性チェック(.immichマーカー確認)
if [ ! -f "${LIBRARY_LOCATION}/.immich" ]; then
    echo -e "  ${YELLOW}⚠ ${LIBRARY_LOCATION}/.immich が見つかりません。起動後にエラーが出る可能性があります${NC}"
fi

# ── バージョン確認 ────────────────────────────────
echo ""
echo "==> [5/5] 設定を確認..."
BACKUP_IMMICH_VERSION=$(grep "^IMMICH_VERSION=" "${BACKUP_DIR}/.env" | cut -d= -f2 || echo "release")
CURRENT_IMMICH_VERSION=$(grep "^IMMICH_VERSION=" "${IMMICH_DIR}/.env" | cut -d= -f2 || echo "release")

if [ "${BACKUP_IMMICH_VERSION}" != "${CURRENT_IMMICH_VERSION}" ]; then
    echo -e "  ${YELLOW}⚠ IMMICHバージョンが異なります${NC}"
    echo "    バックアップ元: ${BACKUP_IMMICH_VERSION}"
    echo "    復元先        : ${CURRENT_IMMICH_VERSION}"
    echo -e "  ${YELLOW}  新環境のバージョン (${CURRENT_IMMICH_VERSION}) をそのまま使用します${NC}"
fi
echo -e "  ${GREEN}✓ 設定確認完了(新環境の .env / docker-compose.yml を維持)${NC}"

# ── Immichコンテナを再起動 ────────────────────────
echo ""
echo "==> Immichを再起動..."
cd "${IMMICH_DIR}"
docker compose up -d

echo "  ⏳ Immich serverの起動を待機中(最大180秒)..."
for i in $(seq 1 36); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:2283/" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" != "000" ]; then
        echo -e "\n  ${GREEN}✓ Immich server 起動完了 (HTTP ${HTTP_CODE})${NC}"
        break
    fi
    if [ "$i" -eq 36 ]; then
        echo -e "\n${RED}ERROR: Immich serverがタイムアウトしました${NC}"
        docker logs immich_server --tail=30
        exit 1
    fi
    sleep 5
    echo -n "."
done

# ── 完了 ──────────────────────────────────────────
NEW_DOMAIN=$(tailscale status --json 2>/dev/null | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('Self', {}).get('DNSName', '').rstrip('.'))
" 2>/dev/null || echo "<新環境のホスト名>")

echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  復元完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 URL  : https://${NEW_DOMAIN}:3307"
echo ""
echo "  ✅ チェックリスト:"
echo "    [ ] ブラウザで https://${NEW_DOMAIN}:3307 にアクセスできるか"
echo "    [ ] ログインできるか(メールアドレス・パスワードは移行元と同じ)"
echo "    [ ] 写真・アルバムが表示されるか"
echo "    [ ] サムネイルが表示されるか(初回は再生成に時間がかかります)"
echo ""
echo "  ⚠ 初回起動時、写真枚数によってはDB内部でのインデックス再構築に時間がかかる場合があります"
echo "    (docker logs immich_server -f で Reindexing の進捗を確認できます)"
echo "  ⚠ 移行元の旧データが退避されている場合:"
echo "    確認後、不要であれば削除してください:"
echo "    ls ${UPLOAD_PARENT}/*.old.* ${LIBRARY_PARENT}/*.old.* 2>/dev/null"
echo ""
echo "════════════════════════════════════════"
echo ""

復元を実行

バックアップしたファイルを指定して復元します。下記はバックアップファイルを/opt/lxd-data/immichに保存した場合の例。

sudo ./immich-restore.sh backup-YYYYMMDD_HHMMSS

外付けHDDなどから復元

バックアップ時と同様に、ホストのマウントパスをコンテナに追加し、その場所を指定して復元を実行します。

cd /opt/lxd-data/immich
sudo ./immich-restore.sh /opt/ssd/backup-YYYYMMDD_HHMMSS

おまけ immich v2 → v3 で大きく変わった点

  • モバイルのレガシー・タイムライン削除
  • DB/MLまわり
    • pgvecto.rs サポート削除DB_VECTOR_EXTENSION=pgvecto.rs はエラー。後継の VectorChord へ移行が推奨)
    • numpy 要件の引き上げ(x86-64-v2 対応の必要など。AVX自体は必須ではない)
    • 環境変数削除(例:IMMICH_MACHINE_LEARNING_PING_TIMEOUTMACHINE_LEARNING_PRELOAD__CLIP 等。Admin UIの設定や別の環境変数へ移行)
  • API/互換性(ブレイキング多め)
    • エラー応答の形式が変更(バリデーションが class-validator → Zod に変更、correlationId は X-Correlation-ID ヘッダへ)
    • アセット関連のDTO変更
      • deviceId / deviceAssetId 削除
      • duration が number で null可(ms)
      • unassignedFaces 削除(代わりに GET /faces
      • width / height が integer に
    • EXIFのDTO変更exifImageWidth/exifImageHeight/iso/rating が integer)
    • アルバムAPIのクエリ変更(例:shared → isShared、さらに isOwned 追加)
    • 共有リンク認証の変更query.password を廃止し、POST /shared-links/login で body.password → cookie利用へ)
    • 共有リンクへの追加挙動が変更(アップロード時に自動で共有リンクへ紐づくようになり、関連APIを削除)
    • 削除エンドポイント多数(例:/assets/random/sync/*/server/theme/assets/exists/assets/device/:deviceIdPUT /assets/:id/original など)
    • 空文字 "" の扱いが厳格化null の代替として使えなくなる)
  • OAuth
    • 不安全なHTTPリクエストがデフォルト拒否(継続するなら oauth.allowInsecureRequests を設定)
  • メトリクス名
    • メトリクス名の _ が . に変更
  • サーバージョブ
    • AuditLogCleanup ジョブ削除

タイトルとURLをコピーしました