SSD Nodes Learn 🎉 VPS $4.99/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-07

ImmichをVPSでバックアップ・復元する方法

Immich v3.1.0のバックアップに必要なオリジナル、PostgresのSQLダンプ、設定ファイルを解説します。データディレクトリのコピーが復元にならない理由と、タイムラインが空になる復元ミスも確認できます。

Immich バックアップに含めるもの

Immich のバックアップには、同じ時点で取得した3つの要素が必要です。UPLOAD_LOCATION 以下のオリジナルファイル、Postgres データベースの SQL ダンプ、スタックを定義する .envdocker-compose.yml です。リストアでは、Immich サーバーを停止した状態で、そのダンプを新しいデータベースに適用します。その後でのみ、スタックの残りを起動します。順序を誤ると、ディスクにはデータがあるのに、タイムラインが空の Immich が起動することになります。

この分離が重要なのは、Immich が互いに認識しない2つの場所に状態を保持するためです。Postgres には、すべてのアルバム、すべての顔クラスタ、すべての共有リンク、すべてのユーザーアカウントと API key、各アセットの保存先パスが格納されています。ファイルシステムには画像データが格納されています。データベースを復元せずにファイルだけを復元すると、Immich には何も表示されません。ファイルを復元せずにデータベースだけを復元すると、すべてのアセットが壊れた画像として表示されます。

ここで使用するコマンドは、2026年8月上旬時点の現行リリースである Immich v3.1.0 を対象にしています。プロジェクトは頻繁にリリースされ、公式のバックアップ手順もこれまでに複数回変更されています。そのため、何かをコピーする前に、実際に実行しているバージョンを確認してください。まだスタックを起動していない場合は、Immich のインストールガイドから始めてください。

パスの参照先を把握する

.env の 2 つの変数が、このページのすべてを決めます。UPLOAD_LOCATION は、Immich がすべてのメディアを書き込む親ディレクトリです。DB_DATA_LOCATION は、Postgres のデータディレクトリです。

標準の example.env では UPLOAD_LOCATION=./library が設定されています。これは分かりにくい既定値です。Immich はその中に library というフォルダーを作成するためです。オリジナルは ./library/library に保存されます。代わりに絶対パスを設定してください。そうすれば、バックアップスクリプトが実行元のディレクトリに依存することはありません。

UPLOAD_LOCATION=/srv/immich/data
DB_DATA_LOCATION=/srv/immich/postgres
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
IMMICH_VERSION=v3.1.0

UPLOAD_LOCATION の中に、Immich は複数のフォルダーを作成します。そのうち 3 つには、どのジョブでも再生成できないデータが保存されます。

  • library: ストレージテンプレートに従って配置されたオリジナル
  • upload: まだテンプレートのレイアウトに移動されていないオリジナルと、処理中のアップロード
  • profile: ユーザープロフィール画像

library を失うと、その写真は失われます。Immich はオリジナルのコピーを別の場所に保存しません。

Postgres のデータディレクトリをコピーしてもバックアップにならない理由

DB_DATA_LOCATION は簡単な対象に見えます。ディレクトリなので、rsync でコピーでき、エラーなしでコピーが完了します。それでもバックアップではありません。確認できる失敗理由が2つあります。

1つ目は、コピー中にデータの整合性が崩れることです。Postgres はすべての変更を最初に write-ahead log (WAL) に書き込み、その後、チェックポイントでテーブルファイルに反映します。そのため、任意の時点でディスク上のファイルは更新途中の状態です。4分かかるコピーでは、最初のファイルを 02:00 に読み、最後のファイルを 02:04 に読みます。この2つのファイルは同じトランザクションに属しません。コピー先のデータで Postgres を起動すると、起動時に PANIC: could not locate a valid checkpoint record で拒否されるか、破損したページを最初に読み取った時点で invalid page in block 1234 of relation base/16384/... により停止します。どちらの場合も、そのコピーから復旧することはできません。

2つ目の理由は、最初にすべてを停止しても解消しません。Postgres のデータディレクトリは、それを書き込んだ正確なバイナリに依存します。Immich はデータベースイメージをダイジェストで固定しており、現在は ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 です。これは、2つのベクトル検索拡張機能を組み込んだ Postgres 14 です。そのビルドで書き込まれたデータディレクトリは、異なる Postgres メジャーバージョンでは開けません。また、異なるバージョンの拡張機能を含むビルドでも開けません。復元先のホストでは、そのイメージを正確に再現する必要があります。SQL ダンプならこの問題はありません。SQL ダンプはテキストなので、互換性のあるサーバーで再実行できます。

pg_dump は、コピー中に整合性が崩れる問題を直接回避します。単一の MVCC (multi-version concurrency control) スナップショット内でデータベース全体を読み取るため、他の書き込みが続いていても、ある1時点のデータベースを正確に取得できます。そのため、ダンプのために Postgres を停止する必要はありません。

バックアップから除外できるもの

これらは再生成できるため、バックアップを省略できます。

  • thumbs: プレビュー画像とサムネイル画像
  • encoded-video: トランスコード済み動画
  • DB_DATA_LOCATION: ダンプから再構築されるデータ
  • model-cache Docker volume: 機械学習モデル。必要なときに再ダウンロードされます

省略にはトレードオフがあり、無条件の利点ではありません。大規模なライブラリでサムネイルとトランスコードを再構築すると、小規模な VPS では CPU を数時間使用します。その間、タイムラインには灰色のプレースホルダーが表示され続けます。再構築は Administration > Jobs から再実行し、「Generate Thumbnails」と「Transcode Videos」を不足しているアセットに対して実行するよう設定します。バックアップ先に空きがある場合は、これらを含めて待ち時間を省いてください。ストレージ容量の上限に近い場合は除外し、再構築を計画してください。Immich ライブラリのサイズ計画では、これらのフォルダーが元データと比べてどの程度増えるかを説明しています。

もう 1 つ、把握しておくべきフォルダーがあります。UPLOAD_LOCATION/backups には Immich が自動生成するデータベースダンプが保存されます。毎日 02:00 に作成され、直近の 14 個が保持されます。この設定は Administration > Settings > Backup で変更できます。これらは容量を消費せず、実際に役立ちます。ただし、保護対象のライブラリと同じディスクに保存されるため、移行の失敗には有効ですが、サーバー自体の故障には役立ちません。自分でダンプも取得してください。自分で実行したダンプは、それに対応するファイルスナップショットと同じ時点で保存されるためです。

データベースダンプを取得する

docker exec -t immich_postgres pg_dump --clean --if-exists \
  --dbname=immich --username=postgres \
  | gzip > /srv/immich/backup/immich.sql.gz

変更している場合は、immichpostgresDB_DATABASE_NAMEDB_USERNAME に置き換えてください。--clean --if-exists はすべての CREATE の前に DROP ... IF EXISTS を付けます。これにより、最初のオブジェクトで停止せず、既存のオブジェクトがあるデータベースにダンプをリプレイできます。

ここで、バックアップスクリプトを気付かないうちに壊す詳細を確認します。このコマンドはパイプラインです。shell は、パイプライン内の最後のコマンドの終了ステータスを返します。誤ったパスワードや起動していないコンテナが原因で pg_dump が失敗しても、gzip は空のストリームを受け取り、正常な gzip ファイルを書き込んで 0 で終了します。スクリプトは成功を記録しますが、実際には 20-byte のバックアップしか残りません。すべてのバックアップスクリプトの先頭に pipefail を記述してください。

#!/usr/bin/env bash
set -euo pipefail

次に、終了コードをそのまま信用せず、結果を確認します。

ls -lh /srv/immich/backup/immich.sql.gz
gunzip -c /srv/immich/backup/immich.sql.gz | head -n 3

正常なダンプの先頭行は -- PostgreSQL database dump です。数百 byte しかないファイルは、スクリプトが何と報告していても失敗したダンプです。

このダンプを書き出した build を、ダンプの隣に記録します。

docker inspect --format '{{.Config.Image}}' immich_server > /srv/immich/backup/immich-version.txt

この用途で .env に依存しないでください。標準のファイルでは IMMICH_VERSION=v3 が設定されています。これはすべての 3.x release に追従する可変タグなので、実際にダンプを書き出した build は分かりません。.env にも正確なタグを固定してください。

サーバーを停止してから、restic でスナップショットを作成する

UPLOAD_LOCATION 配下のファイルは、Immich の実行中は不変ではありません。サーバーは新しいアップロードを書き込み、ストレージテンプレートのジョブはファイルをディレクトリ間で移動します。バックアップツールが書き込み途中のファイルを読み取ると、その時点のバイト列をファイル全体として保存します。この場合、エラーは報告されません。実行中はサーバーコンテナを停止してください。

docker stop immich_server

immich_postgres は実行したままにしてください。ダンプで必要になるためです。サーバーを再び起動するまで、Web インターフェースとモバイルアプリは利用できません。家庭用インスタンスで 03:00 に実行する場合は、通常は問題ありません。

restic は、データがサーバーの外部へ出る前に重複排除と暗号化を行うため、この用途に適しています。サーバー上にないリポジトリを指定してください。

export RESTIC_REPOSITORY=sftp:backup@backup.example.com:/srv/restic/immich
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init

オブジェクトストレージも同じ方法で使用できます。コピーを自分のハードウェアから完全に切り離したい場合は、こちらの方が適しています。

export RESTIC_REPOSITORY=s3:https://s3.example.com/immich-backup
export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key
restic init

このエンドポイントには、2 台目のマシンで自分で運用する MinIO バケットや、任意の S3 互換プロバイダーを指定できます。ライブラリと同じディスク上にあるリポジトリは、誤った削除からは保護できますが、それ以外の障害からは保護できません。

次に、必要な内容だけを指定してスナップショットを作成します。

restic backup \
  /srv/immich/backup/immich.sql.gz \
  /srv/immich/backup/immich-version.txt \
  /srv/immich/data/library \
  /srv/immich/data/upload \
  /srv/immich/data/profile \
  /srv/immich/.env \
  /srv/immich/docker-compose.yml
docker start immich_server

restic は実行のたびにツリー全体を読み取りますが、以前に認識したことのないブロックだけをアップロードします。そのため、最初のスナップショットではライブラリ全体が転送され、それ以降のスナップショットではその日に追加された新しい写真だけが転送されます。

保持期間と、別の場所に保管すべき鍵

restic forget --prune --keep-daily 7 --keep-weekly 5 --keep-monthly 12

forgetはインデックスからスナップショットを削除します。--pruneは、それらのスナップショットが最後の参照だったデータを削除する処理です。--pruneを付けずにforgetを実行すると、ストレージ料金は下がりません。

構造の確認は低コストなので、週に1回実行します。

restic check

これにより、リポジトリのメタデータに一貫性があることを確認できます。データ自体は読み取りません。月に1回はサンプルを再読み取りし、記録済みのハッシュと照合します。

restic check --read-data-subset=5%

これは、ストレージバックエンド上のサイレント破損を検出できる唯一の確認方法です。実際のブロックをダウンロードし、チェックサムを再計算するためです。写真ライブラリに対して完全な--read-dataを実行すると、リポジトリ全体のダウンロードが必要です。従量課金のオブジェクトストレージでは実際の費用が発生するため、実際の運用では対象を順番に一部ずつ確認します。

次に、多くの人が省略する部分です。restic リポジトリのパスワードは復元できません。リセット機能はなく、サポートへの問い合わせでも解決できません。復元対象のサーバー上にある/root/.restic-passwordにコピーが1つだけ保存されている場合、バックアップは暗号化された無意味なデータになります。オブジェクトストレージのアクセスキーと、.envDB_PASSWORDも同様です。これらは、このマシンが稼働していることに依存しない場所に保管してください。紙に印刷して引き出しに入れるか、別のハードウェアで動作するパスワードマネージャーに保存します。そのマネージャーもセルフホストしている場合は、同じ方法で保護する必要があります。また、Vaultwarden のバックアップも独立した作業として扱います。

動作する順序で Immich を復元する

復元の順序によって、適切なバックアップが空のタイムラインになることがあります。新しいホストでは、次の順序に従ってください。

まず設定を復元します。 実行するバージョンと、各パスの参照先が設定されています。

restic restore latest --target /restore \
  --include /srv/immich/.env \
  --include /srv/immich/docker-compose.yml \
  --include /srv/immich/backup

何かを起動する前にバージョンを固定します。 immich-version.txt を読み、.envIMMICH_VERSION にそのタグを正確に設定します。現時点では最新リリースを使用しないでください。Immich はパッチリリース間であってもダウングレードをサポートしません。そのため、新しいサーバーが古いダンプに対して起動し、マイグレーションを実行すると、元に戻す方法がなくなります。

メディアを復元します。

restic restore latest --target /restore --include /srv/immich/data

続いて libraryuploadprofile を移動し、このホスト上で UPLOAD_LOCATION が指すディレクトリの直下に配置します。compose ファイルがそのディレクトリをコンテナ内の固定パスにバインドするため、ホスト側のパス自体は変更できます。ただし、その中のディレクトリ構成は変更できません。

データベースだけを起動します。 DB_DATA_LOCATION は空のままにして、Postgres に新しいクラスタを初期化させます。

cd /srv/immich
docker compose pull
docker compose create
docker start immich_postgres
docker exec immich_postgres pg_isready --username=postgres

初回セットアップが完了すると pg_isreadyaccepting connections を出力します。完了まで数秒かかります。docker compose create はすべてのコンテナを起動せずにビルドします。この手順の目的はそこにあります。Immich サーバーはまだ起動してはいけません。空のデータベースに対してサーバーを起動すると、マイグレーションが適用され、新しいスキーマが作成され、新しい管理者アカウントの作成を求められます。その後、実行中のアプリケーションの下にダンプを復元することになります。

ダンプを復元します。

gunzip --stdout /restore/srv/immich/backup/immich.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 exec -i immich_postgres psql --dbname=immich --username=postgres \
      --single-transaction --set ON_ERROR_STOP=on

この処理では、2 つの部分が重要な役割を果たします。sed が必要なのは、pg_dump が安全対策として出力に空の search_path を書き込むためです。これにより、ダンプ内の修飾されていない名前が予期しないスキーマに解決されるのを防ぎます。Immich のベクトル検索用の型は public にあります。検索パスが空だと、復元処理は vector 型で宣言された最初の列に到達した時点で ERROR: type "vector" does not exist を出力し、psql が停止します。public を検索パスに戻すと解決します。

--single-transaction --set ON_ERROR_STOP=on は復元全体を 1 つのトランザクションで包み、最初のエラーで中断します。データベース全体が復元されるか、変更されていない状態のままになります。これを指定しない場合、途中で失敗すると、起動してログインも受け付けるものの、いくつのアルバムが不足しているか分からないデータベースが残ります。その不足に気付くのは数週間後かもしれません。

ここで、すべてを起動します。

docker compose up -d
docker compose ps
docker logs -f immich_server

Immich Server is listening on のような起動ログが出るまで待ちます。その後、ポート 2283 を開き、以前の認証情報でログインします。ユーザーアカウントはダンプとともに復元されているためです。ログイン画面に最初の管理者アカウントを作成するよう表示された場合、データベースは復元されていません。処理を停止し、psql の出力をもう一度確認してください。

docker compose down -v で始まる公式の復元手順には注意が必要です。-v は名前付きボリュームを削除します。標準の compose ファイルでは、UPLOAD_LOCATIONDB_DATA_LOCATION は bind mount であるため、このコマンドを実行しても保持されます。ただし、どちらかを名前付きボリュームに変更している場合、そのコマンドによって写真が削除されます。コマンドを入力する前に、compose ファイルを確認してください。

復元後にタイムラインが空になる理由

タイムラインは、データベースの行から生成されます。Immich は起動時に写真を再検出するために upload/ を走査しません。行のないファイルには、所有者、日付、アルバムがないためです。そのため、最も多い復元失敗は、ファイルは戻っているのにデータベースがないケースです。Immich は起動すると空のスキーマを作成し、ディスクに写真が大量に残っているにもかかわらず、中身のない動作するインスタンスを起動します。データは失われていません。ただし、何も表示されません。修正するには、上記と同じ手順で、サーバーを停止してからダンプを再適用します。

もう1つのケースは、発生に気付きにくいものです。データベースは復元され、タイムラインにエントリが表示されるものの、すべてのアセットを開けません。これは、通常、libraryuploadprofile が、誰も所定の場所へ移動しなかった restic restore --target /restore の後に、1階層深い場所にあるため、行がコンテナから見えないファイルを指していることを意味します。推測せず、コンテナ内から確認します。

docker exec immich_server ls /data

標準の compose ファイルは UPLOAD_LOCATION/data にマウントします。そのため、この一覧には libraryuploadprofile が表示されるはずです。空のディレクトリや、余分な srv フォルダーが表示される場合、bind mount の参照先が誤った階層になっています。行自体は正しい状態です。

バックアップとリストアのバージョン一致

Immich は頻繁にリリースされ、スキーマもそれに合わせて変更されるため、ダンプには作成元サーバーのスキーマが含まれます。

通常、古いダンプを新しいサーバーにリストアしても動作します。サーバーは起動時に保留中のマイグレーションを適用し、スキーマを段階的に更新するためです。この経路は、リリース順にテストされています。複数のメジャーバージョンを一度にまたぐと問題が発生しやすくなります。プロジェクトでは、破壊的変更をメジャーリリースにまとめ、changelog に記載しています。

新しいダンプを古いサーバーにリストアすることは、まったくできません。ダンプには、古いコードが認識できないテーブルやカラムが含まれるためです。Immich では、patch release 間であってもダウングレードはサポートされていません。実行できるロールバックコマンドもありません。

そのため、安全なリストア手順は単純です。ダンプを作成したものとまったく同じバージョンを実行し、リストアします。ログインして、タイムラインが完全であることを確認してからアップグレードします。1 リリースずつアップグレードし、各更新後に IMMICH_VERSION を更新して docker compose pull && docker compose up -d を実行します。ここでも、1 週間分のダンプを保持すると役立ちます。最新のダンプがアップグレード失敗中に作成されたものでも、前日のダンプがリポジトリに残っているためです。

毎月バックアップを検証する

一度もリストアしていないバックアップは、推測にすぎません。月に1回、使い捨てのインスタンスにリストアし、写真を1枚確認します。この演習は約20分で完了し、このページの残りの内容を復旧計画に変える唯一の方法です。

restic snapshots
restic stats latest

snapshots には昨夜の実行結果が表示されるはずです。stats latest には、数 MB ではなく、ライブラリに近いサイズが表示されるはずです。

リストア先には、できれば予備のホスト上の一時ディレクトリを使用します。

restic restore latest --target /tmp/immich-drill

リストアしたセットから docker-compose.yml.env をコピーし、コピー側で3つ変更します。UPLOAD_LOCATIONDB_DATA_LOCATION/tmp/immich-drill 配下のディレクトリに指定します。Web ポートを別のポートに公開し、2283:2283 の代わりに 12283:2283 を使用します。container_name: の行は削除します。標準の compose ファイルでは immich_server のような名前がハードコードされているため、同じホスト上で2つ目のスタックを起動すると1つ目と衝突し、Docker が作成を拒否するためです。

上記のリストア手順を実行します。まずデータベースだけをリストアし、ダンプを再生してから docker compose up -d を実行します。次に、結果を確認する4つのチェックを行います。

  1. 演習前に使用していたパスワードでログインします。アカウントを使用できれば、ダンプはリストアされています。
  2. タイムラインを開き、最も古い月までスクロールします。日付範囲全体にわたるアセットがあれば、最近の行だけでなく、すべての行が復元されています。
  3. 写真を1枚フルサイズで開き、オリジナルをダウンロードします。
  4. sha256sum を使用して、ライブライブラリ内の同じファイルと比較します。ハッシュが一致すれば、restic を経由した往復後もバイト列が維持されています。

最後に、演習用ディレクトリで docker compose down -v を実行して演習環境を削除し、/tmp/immich-drill も削除します。実施日を目に付く場所に記録してください。この作業の価値は、来月も再度実行することにあります。まだ使用する写真サーバーを決めていない場合は、PhotoPrism と Immich の比較で、まさにこの点における両者の違いを確認できます。

FAQ

Immich をバックアップする前に停止する必要はありますか?

immich_server を停止し、immich_postgres は実行したままにします。データベースを一時停止する必要はありません。pg_dump は 1 つの MVCC スナップショット内を読み取るため、ほかの処理が書き込み中でも、単一の一貫した時点の状態を参照するからです。停止が必要なのはファイル側です。サーバーは新しいアップロードを書き込み、ストレージテンプレートのジョブはディレクトリ間でファイルを移動します。そのため、バックアップツールが書き込み途中のファイルを読み取り、エラーなしで途中までのコピーを保存する可能性があります。スナップショットの前に docker stop immich_server を実行し、その後に docker start immich_server を実行すると、この競合を解消できます。

pg_dump を実行する代わりに Postgres のデータフォルダーをコピーできますか?

いいえ。稼働中のデータディレクトリを順次コピーすると、異なるファイルを異なる時点で読み取るため、結果は一貫した単一の状態になりません。Postgres は起動時に PANIC: could not locate a valid checkpoint record を出して拒否するか、後で破損したページによって失敗します。すべてを停止してコピーした場合でも、コピーは特定のデータベースビルドに結び付いています。Immich は特定のベクトル検索拡張機能のバージョンを含む Postgres 14 イメージを固定しており、そのディレクトリは別の環境では開けません。SQL ダンプはプレーンテキストであり、互換性のある任意のサーバーに再適用できます。

復元後に Immich のタイムラインが空なのはなぜですか?

データベースなしでファイルだけを復元したためです。タイムラインはデータベースの行から構築されます。Immich は写真を再検出するために upload/ をスキャンしないため、対応する行がないファイルは表示されません。写真自体は変更されていません。サーバーを停止し、初期化したばかりの Postgres にダンプを再適用してから、スタックを起動します。タイムラインは完全なのに、すべての写真を開けない場合は、原因が逆です。libraryuploadprofile がコンテナにバインドされたディレクトリ内に直接存在していません。docker exec immich_server ls /data で確認してください。

バックアップで省略できる Immich のフォルダーはどれですか?

thumbsencoded-video はオリジナルから再生成でき、DB_DATA_LOCATION はダンプから再構築できるため、どれもバックアップ対象に含める必要はありません。これらを省略すると、バックアップ前のストレージではなく、復元後の時間を使うことになります。大規模なライブラリのプレビューとトランスコードの再構築には数時間の CPU 時間がかかり、Administration > Jobs から、不足しているアセットを対象に実行されます。決して省略できないのは libraryuploadprofile です。これらには各オリジナルの唯一のコピーが保存されています。

Immich のダンプを新しいバージョンに復元できますか?

通常は可能です。サーバーは起動時に保留中のマイグレーションを適用し、スキーマを順に更新するからです。逆方向は失敗します。Immich はパッチリリース間であってもダウングレードをサポートしていないため、新しいリリースで作成したダンプを古いサーバーに読み込むことはできません。ダンプを書き出したリリースに IMMICH_VERSION を固定して復元し、タイムラインが完全であることを確認してからアップグレードします。各ダンプの横に docker inspect --format '{{.Config.Image}}' immich_server でバージョンを記録してください。デフォルトの IMMICH_VERSION=v3 は浮動タグであり、バージョンを特定できないためです。