Immich 备份与恢复指南:如何确保数据完整性
Immich 备份需包含原始文件、Postgres SQL 转储及配置文件。直接复制 Postgres 数据目录无法保证一致性。本文详解 v3.1.0 版本下的正确恢复顺序,避免因顺序错误导致时间轴显示为空的常见故障。
Immich 备份必须包含的内容
Immich 备份必须同时包含 3 项内容。UPLOAD_LOCATION 下的原始文件。Postgres 数据库的 SQL 转储。用于描述整个堆栈的 .env 和 docker-compose.yml。恢复时,必须先在 Immich server 停止的状态下,将转储导入全新的数据库;只有完成这一步后,才能启动堆栈中的其他服务。顺序错误会导致 Immich 看似正常运行,但时间线为空,而磁盘上却存满了文件。
之所以必须拆分备份,是因为 Immich 将状态保存在两个彼此不了解的位置。Postgres 保存所有相册、所有人脸聚类、所有共享链接、所有用户帐户和 API key,以及每个资源的存储路径。文件系统保存图像数据。只恢复文件而不恢复数据库,Immich 不会显示任何内容。只恢复数据库而不恢复文件,每个资源都会打开为损坏的图像。
这里的命令针对 Immich v3.1.0 编写,这是 2026 年 8 月初的当前版本。截至 2026-10-25,当前版本为 v3.3.5。这是 2026-10-21 发布的安全修复版本,其 compose 文件、数据库镜像以及文档中的备份和恢复步骤与本页使用的内容一致。该项目发布速度很快,文档中的备份流程也发生过多次变化,因此在复制任何内容前,请先确认实际运行的版本。如果堆栈尚未启动,请先阅读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.0Immich 会在 UPLOAD_LOCATION 中创建多个目录。其中有 3 个目录存放任何任务都无法重建的数据:
library:按存储模板排列的原始文件upload:尚未移动到模板目录结构中的原始文件,以及正在上传的文件profile:用户头像
如果丢失 library,照片就无法恢复。Immich 不会在其他位置保留原始文件的第二份副本。
为什么复制 Postgres 数据目录不算备份
DB_DATA_LOCATION 看起来很容易复制。它是一个目录,rsync 可以复制它,而且复制完成时不会报错。但如果在 Postgres 运行期间复制,这份副本不算备份。如果在 Postgres 停止后复制,它可以作为备份,但可靠性较差。原因有两个,下面可以分别看到这两种情况如何失败。
第一个原因是数据撕裂。Postgres 会先将每项更改写入预写式日志(WAL),然后在检查点将更改应用到表文件。因此,在任意时刻,磁盘上的文件都可能处于写入过程中。一次耗时 4 分钟的滚动复制,可能在 02:00 读取第一个文件,在 02:04 读取最后一个文件。这两个文件不属于同一个事务。使用该副本启动 Postgres 时,它可能在启动阶段因 PANIC: could not locate a valid checkpoint record 拒绝启动,也可能启动后在首次读取损坏页面时因 invalid page in block 1234 of relation base/16384/... 退出。这两种情况都无法从该副本恢复。
第二个原因与在所有服务停止后进行的复制有关。PostgreSQL 文档将已关闭数据目录的完整副本定义为有效备份,但该目录只能由存储格式兼容的二进制文件打开。Immich 通过摘要固定其数据库镜像,目前为 ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0。这是一个内置 VectorChord、pgvector 和 pgvecto.rs 向量搜索扩展的 Postgres 14 镜像。Immich 现在使用 VectorChord;该镜像仍保留 pgvecto.rs,是为了让旧安装能够从该扩展迁移,并继续恢复旧备份。较新的 Postgres 14 次要版本可以原样打开该目录,但不同的 Postgres 主版本不行。表所依赖的扩展也必须存在,并且版本兼容。恢复主机必须运行兼容的镜像。SQL 转储不受此限制:它是文本,任何兼容的服务器都可以重放它。
pg_dump 可以直接避免数据撕裂问题。它会在单个 MVCC(多版本并发控制)快照中读取整个数据库,因此即使其他写入继续进行,也能看到某一时刻的完整数据库状态。这就是转储数据库时不必停止 Postgres 的原因。
备份中可以省略的内容
以下内容会自动重新生成,因此可以跳过:
thumbs:预览图和缩略图encoded-video:转码后的视频DB_DATA_LOCATION:可从转储中重新构建model-cacheDocker 卷:机器学习模型,按需重新下载
跳过这些内容是取舍,不是无成本的优化。对于大型媒体库,在小型 VPS 上重新生成缩略图和转码视频可能需要数小时 CPU 时间,期间时间线会一直显示灰色占位符。可以前往“管理 > 作业”,将“生成缩略图”和“转码视频”设置为仅处理缺少的资源,然后重新运行这些作业。如果备份目标有足够空间,请将这些内容纳入备份,避免等待。如果存储空间接近上限,请将其省略,并为重建预留时间。计算 Immich 媒体库大小介绍了这些目录相对于原始文件会增长到多大。
还有一个值得了解的目录。UPLOAD_LOCATION/backups 保存 Immich 自动生成的数据库转储。系统每天 02:00 写入一个,并保留最近 14 个;可在“管理 > 设置 > 数据库转储设置”中配置。这些转储不占用额外操作成本,而且确实很有用。但它们与所保护的媒体库位于同一块磁盘上,因此适合应对迁移失败,不适合应对服务器磁盘损坏。仍然要自行执行转储,因为手动触发的转储会与对应的文件快照在同一时刻生成。
导出数据库转储文件
docker exec -t immich_postgres pg_dump --clean --if-exists \
--dbname=immich --username=postgres \
| gzip > /srv/immich/backup/immich.sql.gz如果修改过 immich 和 postgres,请将其替换为您的 DB_DATABASE_NAME 和 DB_USERNAME。--clean --if-exists 会在每个 CREATE 前添加一个 DROP ... IF EXISTS,这样即使数据库中已存在对象,转储文件在导入时也不会因报错而停止。
接下来是备份脚本中容易被忽视的细节。该命令是一个管道操作,而 Shell 只会报告管道中最后一个命令的退出状态。如果 pg_dump 失败(例如密码错误或容器未运行),gzip 将接收到空流,并生成一个看似有效的 gzip 文件,随后以状态码 0 退出。此时脚本会记录备份成功,但您实际得到的只是一个 20 字节的无效备份。请在每个备份脚本的开头添加 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。无论脚本输出什么,如果文件只有几百字节,说明转储已失败。
通过查询运行中的服务器,在转储文件旁记录生成该备份的版本号:
curl -fsS http://localhost:2283/api/server/version > /srv/immich/backup/immich-version.txt该接口无需登录,会返回主版本号、次版本号和补丁号,例如 3、1 和 0 表示 v3.1.0。不要依赖 .env 来获取此信息,也不要使用 docker inspect --format '{{.Config.Image}}' immich_server,因为它只会返回您配置的镜像引用。官方文件通常设置 IMMICH_VERSION=v3,这是一个跟随所有 3.x 版本的浮动标签,因此以 :v3 结尾的记录无法说明具体是哪个构建版本生成的转储。请务必在 .env 中锁定确切的标签。
暂停服务器,然后使用 restic 创建快照
Immich 运行时,UPLOAD_LOCATION 下的文件不是不可变的。服务器会写入新上传的文件,存储模板任务也会在目录之间移动文件。如果备份工具在文件写入到一半时读取它,就会将当时读取到的字节保存为完整文件,而不会报告任何错误。请在整个备份过程中停止服务器容器:
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 存储桶,也可以是任何兼容 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_serverrestic 每次运行都会读取整个目录树,但只上传之前未见过的块。因此,第一次快照会传输整个媒体库;此后的每个快照只传输当天新增的照片。
保留策略,以及必须存放在其他位置的密钥
restic forget --prune --keep-daily 7 --keep-weekly 5 --keep-monthly 12forget 会从索引中移除快照。--prune 才会删除这些快照最后引用的数据。运行 forget 时不使用 --prune,存储费用就不会下降。
结构检查成本很低,因此每周运行一次:
restic check该命令会验证仓库元数据是否一致,但不会读取数据。每月重新读取一部分数据,并将其与记录的哈希值进行校验:
restic check --read-data-subset=5%只有这项检查能发现存储后端上的静默损坏,因为它会下载实际数据块并重新计算校验和。对照片库执行完整的 --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 设置为该确切版本(例如 v3.1.0 代表主版本 3、次版本 1、补丁 0),暂时不要使用最新版本。Immich 不支持降级,即使是补丁版本之间也不支持。如果较新的服务器在旧转储文件上启动并运行了迁移,将无法回退。
恢复媒体文件。
restic restore latest --target /restore --include /srv/immich/data然后移动 library、upload 和 profile,使其直接位于该主机上 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=postgrespg_isready 会在首次设置完成后打印 accepting 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其中两部分操作至关重要。sed 的存在是因为 pg_dump 会在输出中写入一个空的 search_path 作为安全措施,防止转储中的非限定名称解析到意外的架构中。Immich 的向量搜索类型位于 public 中,因此如果搜索路径为空,恢复过程在遇到第一个声明为向量类型的列时,psql 会因 ERROR: type "vector" does not exist 而停止。将 public 放回路径即可解决此问题。
--single-transaction --set ON_ERROR_STOP=on 将整个恢复过程封装在一个事务中,一旦出现错误即中止。这样您要么得到一个完整的数据库,要么得到一个未被触动的数据库。如果没有此项,恢复中途失败会导致数据库虽然能启动并接受登录,但会丢失数量不明的相册,而您可能几周后才会发现。
现在启动所有服务。
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_LOCATION 和 DB_DATA_LOCATION 是绑定挂载,因此不会被删除。如果您将其中任何一个更改为命名卷,该命令会删除您的照片。在输入命令前,请务必阅读您的 compose 文件。
恢复后时间线为空的原因
时间线来自数据库中的记录。Immich 启动时不会遍历 upload/ 来重新发现照片,因为没有数据库记录的文件没有所属关系、日期或相册。因此,最常见的错误恢复情况是文件已恢复,但数据库缺失。Immich 启动后会创建空架构,并提供一个可以正常运行但没有任何内容的实例,而磁盘中仍然存有您的照片。没有任何数据丢失,但这些数据也不会显示。解决方法是停止服务器后重新导入转储文件,具体操作与上文完全相同。
第二种情况不太明显。数据库已恢复,时间线也填充了条目,但每个资源都无法打开。这表示数据库记录指向容器无法看到的文件。通常是因为在执行 restic restore --target /restore 后,没有人将 library、upload 和 profile 移动到正确位置,导致它们多嵌套了一层。不要猜测,直接从容器内部检查:
docker exec immich_server ls /data默认的 compose 文件会将 UPLOAD_LOCATION 挂载到 /data,因此该目录列表应显示 library、upload 和 profile。如果显示空目录或多余的 srv 目录,说明绑定挂载指向了错误的层级,而数据库记录本身没有问题。
备份与恢复的版本匹配
Immich 发布频繁,数据库架构也会随版本变化,因此转储包含生成它的服务器所使用的架构。
将旧转储恢复到较新的服务器通常可以成功,因为服务器启动时会应用待执行的迁移,使架构向前更新。Immich 自己的备份文档仍建议尽量恢复到兼容版本,因为版本不匹配意味着恢复期间会执行迁移。该项目会将破坏性变更保留在主版本升级中,并在发行说明中记录这些变更,因此跨主版本恢复时应首先查阅相关说明。
将较新的转储恢复到较旧的服务器完全不可行。转储包含旧代码不认识的表和列,而且 Immich 明确表示不支持降级,即使是在补丁版本之间也不支持。没有可用于回滚的命令。
因此,安全的恢复流程很简单。运行生成转储的精确版本,重放转储,登录并确认时间线完整,然后再升级。按照官方升级页面说明进行升级:阅读发行说明中的破坏性变更,将 IMMICH_VERSION 设置为 .env 中的目标版本,然后运行 docker compose pull && docker compose up -d。同时保留一周的转储也很有帮助:如果最新转储是在一次失败的升级期间生成的,昨天的转储仍保存在仓库中。
每月验证备份
从未恢复过的备份只能算猜测。每月将备份恢复到一个可随时丢弃的实例中,并查看一张照片。整个演练大约需要 20 分钟;只有完成这一步,本页其余内容才真正构成恢复方案。
restic snapshots
restic stats latestsnapshots 应列出昨晚的运行记录。stats latest 显示的大小应接近您的图库,而不是只有几 MB。
将备份恢复到临时目录,最好使用备用主机:
restic restore latest --target /tmp/immich-drill从恢复的数据集中复制 docker-compose.yml 和 .env,然后在副本中修改 3 项内容。将 UPLOAD_LOCATION 和 DB_DATA_LOCATION 指向 /tmp/immich-drill 下的目录。将 Web 端口发布到其他位置,使用 12283:2283 而不是 2283:2283。删除 container_name: 行,因为默认的 compose 文件会硬编码 immich_server 等名称。在同一台主机上运行第二个堆栈时,这会与第一个堆栈冲突,导致 Docker 无法创建它。
按照上文的恢复流程执行:仅恢复数据库,重放转储,然后执行 docker compose up -d。现在执行以下 4 项检查,以确认恢复确实有效。
- 使用演练前的密码登录。账户可以正常使用,说明转储已成功恢复。
- 打开时间线并滚动到最早的月份。整个日期范围内都有资源,说明所有数据行都已恢复,而不只是最近的数据。
- 以原始大小打开一张照片,并下载原始文件。
- 使用
sha256sum将其与在线图库中的同一文件进行比较。哈希值一致,说明文件内容完整地经过了 restic 的备份恢复流程。
然后在演练目录中使用 docker compose down -v 清理演练,并删除 /tmp/immich-drill。将日期记录在您能看到的位置,因为这项工作的价值完全在于下个月再次执行。如果您仍在决定采用哪款照片服务器,PhotoPrism 与 Immich 的对比会说明两者在这一点上的具体差异。
FAQ
备份 Immich 时必须停止服务吗?
停止 immich_server,但保持 immich_postgres 运行。数据库无需暂停,因为 pg_dump 在单个 MVCC 快照内读取数据,无论此时是否有其他写入操作,它都能看到一致的瞬时状态。必须停止服务的原因是文件:服务器会写入新的上传内容,存储模板作业也会在目录间移动文件,备份工具可能会在文件写入过程中读取,从而导致备份副本不完整且不报错。在快照前执行 docker stop immich_server,并在快照后执行 docker start immich_server,可以消除这种竞态条件。
我可以直接复制 Postgres 数据文件夹而不是运行 pg_dump 吗?
Postgres 运行时不能这样做。对运行中的数据目录进行滚动复制会读取不同时刻的文件,导致备份状态不一致;Postgres 在启动时会因 PANIC: could not locate a valid checkpoint record 拒绝加载,或在后续运行中因页面损坏而失败。在停止 Postgres 后进行完整复制是有效的备份,但它只能在相同的主版本且扩展兼容的 Postgres 环境下打开。Immich 锁定了包含 VectorChord、pgvector 和 pgvecto.rs 的 Postgres 14 镜像,该目录无法在其他主版本下打开。SQL 转储是纯文本格式,可重播到任何兼容的服务器中,因此它仍是首选的备份方式。
为什么恢复后我的 Immich 时间线是空的?
因为时间线是根据数据库行构建的,而你只恢复了文件,没有恢复数据库。Immich 从不扫描 upload/ 来重新发现照片,因此没有对应数据库行的文件将保持不可见。照片本身并未受损。请停止服务器,将转储文件重播到新初始化的 Postgres 中,然后启动整个堆栈。如果时间线显示正常但所有照片都无法打开,则情况相反:library、upload 和 profile 没有直接位于绑定到容器的目录内。请使用 docker exec immich_server ls /data 进行检查。
备份时可以跳过哪些 Immich 文件夹?
thumbs 和 encoded-video 可以从原始文件重新生成,DB_DATA_LOCATION 可以从转储文件重建,因此它们都不必包含在备份集中。跳过这些文件夹意味着在恢复后需要花费时间,而不是在备份前节省存储空间,因为为大型库重建预览图和转码文件需要数小时的 CPU 时间,需通过 Administration > Jobs 针对缺失资源运行。绝对不能跳过的是 library、upload 和 profile,它们保存了每一张原始照片的唯一副本。
我可以将 Immich 转储文件恢复到较新的版本吗?
通常可以,因为服务器会在启动时应用待处理的迁移并升级架构。反向操作则会失败:Immich 不支持降级,即使是补丁版本之间也不支持,因此较新版本生成的转储文件无法加载到较旧的服务器中。请使用锁定在生成转储文件时所用版本的 IMMICH_VERSION 进行恢复,确认时间线完整后再进行升级。请记录每个转储文件对应的版本号并存放在 curl -fsS http://localhost:2283/api/server/version 旁边,因为 docker inspect 只会显示配置的镜像引用,而默认的 IMMICH_VERSION=v3 是一个浮动标签,无法提供任何版本信息。