SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

Immich VPS 備份與還原:避免時間軸變空

了解 Immich 備份必須包含原始檔案、Postgres SQL 傾印與設定檔,為何 live Postgres 資料目錄不算備份,以及還原順序錯誤會讓時間軸空白。

Immich 備份必須包含的內容

Immich 備份必須在同一時間擷取 3 項內容。UPLOAD_LOCATION 下的原始檔。Postgres 資料庫的 SQL 傾印。描述整個堆疊的 .env 與 docker-compose.yml。還原時,必須在 Immich server 停止的狀態下,將該傾印匯入新的資料庫,並在此之後才啟動堆疊的其餘部分。順序錯誤時,最後可能得到一個運作正常的 Immich,但時間軸為空,底層卻有完整的磁碟資料。

這種拆分很重要,因為 Immich 將狀態存放在 2 個彼此不互通的位置。Postgres 儲存每個相簿、每個臉部分群、每個共享連結、每個使用者帳號與 API key,以及每項資產的儲存路徑。檔案系統則儲存影像內容。只還原檔案而不還原資料庫,Immich 不會顯示任何內容。只還原資料庫而不還原檔案,每項資產都會開啟為損壞的影像。

這裡的命令以 Immich v3.1.0 為準,也就是 2026 年 8 月初的當時版本。截至 2026-10-21,目前版本為 v3.3.4,於 2026-10-20 發布;其 compose file、database image,以及文件所述的備份與還原步驟,都與本頁使用的內容相符。此專案發布速度很快,文件中的備份程序也曾多次變更,因此在複製任何內容前,請先確認實際執行的版本。如果堆疊尚未啟動,請先參閱 Immich 安裝指南,再返回本頁。

了解路徑的實際指向

.env 中的兩個變數會決定本頁的所有內容。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

Immich 會在 UPLOAD_LOCATION 中建立數個資料夾。其中 3 個資料夾存放任何工作都無法重建的資料:

  • library:依您的儲存範本排列的原始檔案
  • upload:尚未移入範本配置的原始檔案,以及正在上傳的檔案
  • profile:使用者個人資料圖片

遺失 library 後,照片就會消失。Immich 不會在其他位置保留原始檔案的第二份副本。

為什麼複製 Postgres 資料目錄不算備份

DB_DATA_LOCATION看起來很容易處理。它是一個目錄,rsync可以複製它,而且複製完成時不會發生錯誤。如果在 Postgres 執行期間複製,結果不算備份。如果停止 Postgres 後複製,結果雖然算備份,但可靠性很低。原因有兩個,以下也會說明這兩種情況如何失敗。

第一個原因是資料撕裂。Postgres 會先將每項變更寫入 write-ahead log (WAL),再於 checkpoint 將變更套用到資料表檔案。因此,磁碟上的檔案在任何時刻都可能處於寫入過程中;如果複製作業耗時 4 分鐘,可能在 02:00 讀取第一個檔案,在 02:04 讀取最後一個檔案。這兩個檔案不屬於同一筆交易。當你使用複製結果啟動 Postgres 時,它可能在啟動時以 PANIC: could not locate a valid checkpoint record 拒絕啟動,也可能啟動後在第一次讀取損毀頁面時以 invalid page in block 1234 of relation base/16384/... 終止。這兩種情況都無法從該複本復原。

第二個原因與在所有服務停止時建立的複本有關。PostgreSQL 文件指出,完整複製已關閉的資料目錄是有效的備份,但該目錄只能由儲存格式相容的 binary 開啟。Immich 會以 digest 鎖定其資料庫映像,目前為 ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0。這是內建 VectorChord、pgvector 和 pgvecto.rs 向量搜尋擴充功能的 Postgres 14。較新的 Postgres 14 次要版本可以直接開啟該目錄,但不同的 Postgres 主要版本不行。資料表所依賴的擴充功能也必須存在,且版本相容。復原主機必須執行相容的映像。SQL dump 不受此限制:它是文字格式,任何相容的伺服器都能重新執行其中的內容。

pg_dump會直接避開資料撕裂問題。它會在單一 MVCC (multi-version concurrency control) snapshot 中讀取整個資料庫,因此即使其他寫入作業持續進行,也能看到資料庫在某一個時間點的完整狀態。這就是不必停止 Postgres 也能建立 dump 的原因。

可略過的備份內容

這些內容可以重新產生,因此可以略過:

  • thumbs:預覽圖與縮圖
  • encoded-video:轉碼後的影片
  • DB_DATA_LOCATION:由傾印檔重新建立的內容
  • model-cache Docker volume:機器學習模型,會在需要時重新下載

略過這些內容是取捨,不是沒有代價。大型媒體庫重新建立縮圖與轉碼內容,可能會讓小型 VPS 持續耗用數小時的 CPU;在此期間,時間軸會一直顯示灰色預留位置。你可以從 Administration > Jobs 重新執行,並將「Generate Thumbnails」與「Transcode Videos」設定為只處理缺少的資產。若備份目標有足夠空間,請一併納入,以免等待重建。若儲存空間接近上限,則可略過,並預留重建所需的時間。Immich 媒體庫容量規劃說明這些資料夾相對於原始檔案會增長到多大。

還有一個資料夾值得了解。UPLOAD_LOCATION/backups 儲存 Immich 自動建立的資料庫傾印檔,每日 02:00 寫入,保留最近 14 份;可在 Administration > Settings > Database Dump Settings 中設定。這些檔案不會增加額外成本,而且確實很有用。但它們與受保護的媒體庫位於同一張磁碟,因此適合處理錯誤的遷移,不適合處理伺服器故障。無論如何,仍應自行建立傾印檔,因為你手動觸發的傾印檔,會與對應的檔案快照在同一時間產生。

擷取資料庫傾印

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

如果你修改過 DB_DATABASE_NAME 和 DB_USERNAME,請將 immich 和 postgres 替換為目前使用的值。--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。無論指令碼如何回報,只有幾百 bytes 的檔案都表示傾印失敗。

向執行中的伺服器查詢寫入傾印的 release,並將結果記錄在傾印檔案旁:

curl -fsS http://localhost:2283/api/server/version > /srv/immich/backup/immich-version.txt

此 endpoint 不需要登入,會以 major、minor 和 patch numbers 回傳目前執行中的 release,因此 3、1 和 0 代表 v3.1.0。不要使用 .env 判斷,也不要使用 docker inspect --format '{{.Config.Image}}' immich_server;後者只會回傳你設定的 image reference。預設檔案會設定 IMMICH_VERSION=v3,這是會隨每個 3.x release 更新的浮動 tag,因此記錄若以 :v3 結尾,無法得知實際寫入傾印的 build。也請在 .env 中固定使用確切的 tag。

暫停伺服器,再使用 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

該端點可以是您在第二台機器上自行執行的 MinIO bucket,也可以是任何與 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 則會刪除那些快照曾是其最後參照的資料。執行 forget 時若未加上 --prune,儲存費用就不會下降。

結構檢查成本低,因此每週執行一次:

restic check

這會驗證儲存庫中繼資料是否一致,但不會讀取資料。每月重新讀取一部分資料,並與記錄的雜湊值比對:

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

只有這項檢查能偵測儲存後端的靜默損毀,因為它會下載實際區塊並重新計算其 checksum。對相片資料庫執行完整的 --read-data,代表要下載整個儲存庫。若使用按量計費的物件儲存,這會產生實際費用。因此,實務上通常採用輪替檢查的子集。

接下來是許多人會略過的部分。restic 儲存庫密碼無法復原。 沒有重設機制,也無法提交支援工單。如果唯一的副本位於你要進行還原的伺服器上的 /root/.restic-password,備份內容就只是一堆無法解密的資料。物件儲存存取金鑰,以及 .env 中的 DB_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,將 .env 中的 IMMICH_VERSION 設為完全相同的版本(major 3、minor 1、patch 0 為 v3.1.0),目前先不要使用最新版本。Immich 不支援降級,即使是 patch release 之間也不支援。因此,如果較新的 server 針對較舊的傾印啟動並執行 migration,就無法復原。

還原媒體檔案。

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

接著移動 library、upload 與 profile,使其直接位於本主機上 UPLOAD_LOCATION 所指向的目錄內。主機上的路徑本身可以變更,因為 compose file 會將該目錄繫結至 container 內的固定路徑。但目錄內的結構不能變更。

單獨啟動資料庫。 讓 DB_DATA_LOCATION 保持空白,使 Postgres 初始化新的 cluster。

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

初次設定完成後,pg_isready 會輸出 accepting connections;這需要幾秒鐘。docker compose create 會建立所有 container,但不啟動它們。這正是此步驟的目的:Immich server 目前不能執行。若 server 針對空資料庫啟動,就會套用 migration、建立新的 schema,並要求你建立新的 admin account;此時你會在正在執行的應用程式下方重新匯入傾印。

重新匯入傾印。

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 作為安全措施,避免傾印中的未限定名稱解析到非預期的 schema。Immich 的向量搜尋型別位於 public。因此,若 search path 為空,還原程序遇到第一個宣告為 vector 型別的欄位時,psql 會以 ERROR: type "vector" does not exist 停止。將 public 加回 search path 即可修正問題。

--single-transaction --set ON_ERROR_STOP=on 會將整個還原程序包在單一 transaction 中,並在第一個錯誤發生時中止。結果不是完整資料庫,就是未變更的資料庫。若沒有這個選項,程序在中途失敗後會留下仍可啟動並接受登入的資料庫,但其中可能缺少數量不明的相簿,而你可能要到幾週後才會發現。

現在啟動所有服務。

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

等待類似 Immich Server is listening on 的啟動訊息,然後開啟 port 2283,並使用舊的憑證登入,因為使用者帳號已隨傾印還原。如果登入頁面改為要求建立第一個 admin account,表示資料庫未成功還原。請停止操作,重新查看 psql 輸出。

最後提醒官方 command-line 還原指示。該指示以 docker compose down -v 開頭。-v 會移除具名 volume。在預設 compose file 中,UPLOAD_LOCATION 與 DB_DATA_LOCATION 是 bind mount,因此會保留。如果你將其中任何一個改為具名 volume,該命令會刪除照片。執行前請先讀取 compose file。

還原後時間軸為何是空的

時間軸是根據資料庫資料列建立的。Immich 啟動時不會掃描 upload/ 來重新尋找相片,因為沒有資料列的檔案沒有擁有者、日期或相簿。因此,最常見的錯誤還原情況是檔案已還原,但資料庫遺失。Immich 會啟動並建立空的 schema,讓你取得可運作的執行個體,但其中沒有任何內容,而磁碟上仍存放著所有相片。資料沒有遺失,但也不會顯示。修正方式是在伺服器停止時重新套用 dump,方式與上述完全相同。

第二種情況較不明顯。資料庫已還原,時間軸也出現項目,但每個資產都無法開啟。這表示資料列指向容器無法存取的檔案,通常是因為執行 restic restore --target /restore 後,沒有人將 library、upload 和 profile 移到正確位置,導致它們多了一層目錄。請從容器內部檢查,不要猜測:

docker exec immich_server ls /data

預設的 compose 檔案會將 UPLOAD_LOCATION 掛載至 /data,因此該清單應顯示 library、upload 和 profile。如果顯示空目錄或多出的 srv 目錄,表示 bind mount 指向錯誤的層級,而資料列本身沒有問題。

備份與還原的版本一致性

Immich 經常發布新版本,資料庫結構也會隨之變更,因此 dump 會保留產生該 dump 的伺服器所使用的資料庫結構。

將較舊的 dump 還原至較新的伺服器通常可以成功,因為伺服器啟動時會套用待處理的 migration,逐步更新資料庫結構。這條路徑會依照各版本的發布順序進行測試。一次跨越多個主版本時較容易發生問題,因為專案會在主版本中加入破壞性變更,並記錄於 changelog。

將較新的 dump 還原至較舊的伺服器則完全無法運作。dump 包含較舊程式碼不認得的資料表與欄位,而 Immich 表示即使在 patch release 之間也不支援降級。沒有可用的 rollback command 可執行。

因此,安全的還原方式很單純。執行產生該 dump 的確切版本,還原 dump,登入後確認時間軸完整,之後才進行升級。一次只升級一個 release,逐次更新 IMMICH_VERSION,並在每次更新後執行 docker compose pull && docker compose up -d。保留一週的 dump 也很有幫助:如果最新的 dump 原來是在升級失敗期間建立的,昨天的 dump 仍會保留在 repository 中。

每月驗證備份

從未還原過的備份只是猜測。每月將備份還原到一次性使用的 instance,並開啟一張照片查看。這項演練約需 20 分鐘,也是唯一能將本頁其餘內容轉化為復原計畫的步驟。

restic snapshots
restic stats latest

snapshots 應列出昨晚的執行結果。stats latest 應顯示接近相片庫的大小,而不是只有幾 MB。

將備份還原到暫存目錄,最好使用備用主機:

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

從還原後的集合複製 docker-compose.yml 和 .env,然後在副本中修改 3 項設定。將 UPLOAD_LOCATION 和 DB_DATA_LOCATION 指向 /tmp/immich-drill 下的目錄。將 web port 發布到其他位置,使用 12283:2283 取代 2283:2283。刪除 container_name: 行,因為原始 compose 檔案會硬編碼 immich_server 等名稱。如此一來,同一主機上的第二個 stack 會與第一個衝突,Docker 也會拒絕建立它。

執行上方的還原程序:先只還原資料庫,再重播 dump,最後執行 docker compose up -d。接著進行以下 4 項檢查,確認還原確實成功。

  1. 使用演練前的密碼登入。帳號可以正常使用,表示 dump 已成功還原。
  2. 開啟時間軸並捲動到最早的月份。整個日期範圍內都有資產,表示所有資料列都已還原,而不只是最近的資料。
  3. 以完整尺寸開啟一張照片,並下載原始檔案。
  4. 使用 sha256sum,將該檔案與正式相片庫中的相同檔案比較。雜湊值一致,表示檔案內容已完整通過 restic 的往返流程。

接著在演練目錄中使用 docker compose down -v 清除演練環境,並刪除 /tmp/immich-drill。將日期記錄在你會看到的地方,因為這項工作的價值完全取決於下個月再次執行。如果你仍在決定要採用哪個相片伺服器,PhotoPrism 與 Immich 的比較會說明兩者在這項能力上的差異。

FAQ

是否必須停止 Immich 才能進行備份?

停止 immich_server,讓 immich_postgres 持續執行。資料庫不需要暫停,因為 pg_dump 會在單一 MVCC snapshot 中讀取,因此無論其他程序正在寫入什麼,都能看到同一個一致時間點的資料。需要停止的是檔案服務:伺服器會寫入新的上傳檔案,而 storage template 工作也會在目錄之間移動檔案。因此,備份工具可能在檔案寫入一半時讀取,並在沒有任何錯誤的情況下儲存截斷的副本。在 snapshot 前執行 docker stop immich_server,並在完成後執行 docker start immich_server,即可排除這項競態問題。

可以直接複製 Postgres 資料目錄,而不執行 pg_dump 嗎?

Postgres 執行時不可以。對執行中的資料目錄進行逐步複製,會在不同時間點讀取不同檔案,因此結果不是單一一致狀態。Postgres 啟動時會以 PANIC: could not locate a valid checkpoint record 拒絕這類副本,或稍後因頁面損毀而失敗。Postgres 停止後取得的完整副本是有效備份,但只能在相同的 Postgres major version 搭配相容 extensions 的環境中開啟。Immich 固定使用內含 VectorChord、pgvector 和 pgvecto.rs 的 Postgres 14 image,該資料目錄無法在不同 major version 下開啟。SQL dump 是純文字,可還原至任何相容伺服器,因此仍應保留 SQL dump 副本。

為什麼還原後 Immich timeline 是空的?

因為 timeline 是由資料庫資料列建立,而你只還原了檔案,沒有還原資料庫。Immich 不會掃描 upload/ 來重新找回相片,因此沒有資料列對應的檔案會維持不可見。相片本身沒有受損。停止伺服器,將 dump 還原至新初始化的 Postgres,然後啟動 stack。若 timeline 完整,但每張相片都無法開啟,問題則相反:library、upload 和 profile 並不在綁定至 container 的目錄中。使用 docker exec immich_server ls /data 檢查。

備份時可以略過哪些 Immich 目錄?

thumbs 和 encoded-video 可由 originals 重新產生,而 DB_DATA_LOCATION 可由 dump 重建,因此這些項目都不必納入備份集。略過它們,是將時間成本放在還原後,而不是在還原前增加儲存空間需求;大型媒體庫的 previews 和 transcodes 需要數小時 CPU 時間,可透過 Administration > Jobs,針對缺少的 assets 執行重建。絕對不可略過的是 library、upload 和 profile,因為其中保存每個 original 的唯一副本。

可以將 Immich dump 還原至較新的版本嗎?

通常可以,因為伺服器會在啟動時套用待處理的 migrations,逐步更新 schema。反向操作則會失敗:Immich 不支援降級,即使是 patch releases 之間也不支援,因此無法將較新 release 建立的 dump 載入較舊伺服器。使用固定為建立該 dump 之 release 的 IMMICH_VERSION 進行還原,確認 timeline 完整後再升級。使用 curl -fsS http://localhost:2283/api/server/version 將 release 記錄在每個 dump 旁,因為 docker inspect 只會回傳已設定的 image reference,而預設的 IMMICH_VERSION=v3 是浮動 tag,無法提供版本資訊。