Immich VPS 备份与恢复:Postgres 转储和时间线为空
Immich v3.1.0 VPS 备份必须同时包含原始文件、Postgres SQL 转储和配置文件。了解为何复制数据目录无效,以及错误恢复顺序如何导致时间线为空。
Immich 备份必须包含什么
Immich 备份必须包含在同一时刻获取的三部分内容。UPLOAD_LOCATION 下的原始文件。Postgres 数据库的 SQL 转储。用于描述整个堆栈的 .env 和 docker-compose.yml。恢复时,必须在 Immich 服务器停止期间,将该转储导入全新的数据库;完成后才能启动堆栈中的其他服务。如果顺序错误,最终可能得到一个运行正常但时间线为空的 Immich,而磁盘中实际存有完整数据。
这种拆分很重要,因为 Immich 将状态保存在两个彼此不了解的位置。Postgres 保存所有相册、所有人脸集群、所有共享链接、所有用户帐户和 API 密钥,以及每个资源的存储路径。文件系统保存图像数据。只恢复文件而不恢复数据库时,Immich 不会显示任何内容。只恢复数据库而不恢复文件时,每个资源都会打开为损坏的图像。
这里的命令针对 Immich v3.1.0 编写,该版本是 2026 年 8 月初的当前版本。该项目发布速度很快,官方记录的备份流程也不止一次发生变化,因此复制任何内容前,请先确认实际运行的版本。如果堆栈尚未启动,请先阅读 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 会先将每项更改写入预写式日志(WAL),然后在检查点将更改应用到表文件。因此,在任意时刻,磁盘上的文件都可能处于写入过程中。一次耗时 4 分钟的滚动复制可能在 02:00 读取第一个文件,在 02:04 读取最后一个文件。这两个文件不属于同一事务。使用复制结果启动 Postgres 时,它可能在启动阶段因 PANIC: could not locate a valid checkpoint record 拒绝启动,也可能启动后在首次读取损坏页面时因 invalid page in block 1234 of relation base/16384/... 退出。通过该复制结果无法恢复这两种故障。
第二个原因即使在先停止所有服务后仍然存在。Postgres 数据目录与写入它的确切二进制文件绑定。Immich 当前通过摘要固定其数据库镜像,摘要为 ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0。这是包含两个向量搜索扩展的 Postgres 14。由该构建写入的数据目录无法在其他 Postgres 主版本下打开,也无法在扩展版本不同的构建下打开。恢复主机必须完全复现该镜像。SQL 转储不受此限制:它是文本,任何兼容的服务器都可以重放它。
pg_dump直接规避了数据撕裂问题。它会在单个 MVCC(多版本并发控制)快照中读取整个数据库,因此即使其他写入同时进行,也能看到数据库在某一时刻的完整状态。这就是导出数据库时不必停止 Postgres 的原因。
可从备份中排除的内容
以下内容可以重新生成,因此可以跳过:
thumbs:预览图和缩略图encoded-video:转码后的视频DB_DATA_LOCATION:从转储重新构建的内容model-cacheDocker 卷:机器学习模型,按需重新下载
跳过这些内容是权衡,不是无成本的优化。对于大型媒体库,在小型 VPS 上重新生成缩略图和转码文件可能需要数小时的 CPU 时间,期间时间线会一直显示灰色占位符。您可以在 Administration > Jobs 中重新运行这些任务,并将 “Generate Thumbnails” 和 “Transcode Videos” 设置为仅处理缺失的资源。如果备份目标有足够空间,请将这些内容纳入备份,避免等待。如果存储空间接近上限,请排除它们,并为重建过程预留时间。估算 Immich 媒体库大小介绍了这些目录相对于原始文件会增长到多大。
还有一个目录值得了解。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如果您修改过 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。无论脚本报告什么,只有几百字节的文件都表示转储失败。
在转储文件旁记录生成该文件的构建版本:
docker inspect --format '{{.Config.Image}}' immich_server > /srv/immich/backup/immich-version.txt不要依赖 .env。默认文件将 IMMICH_VERSION=v3 设置为一个浮动标签,该标签会跟随每个 3.x 版本,因此无法告诉您实际生成转储的具体构建版本。还应在 .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 设置为完全一致的标签,暂时不要使用最新版本。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=postgres首次设置完成后,pg_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_VERSION 后运行 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 启动时会因 PANIC: could not locate a valid checkpoint record 拒绝使用该副本,或者稍后因页面损坏而失败。即使在所有服务停止后复制,该副本也与确切的数据库构建版本绑定:Immich 固定使用带有特定向量搜索扩展版本的 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 进行恢复,确认时间线完整后再升级。使用 docker inspect --format '{{.Config.Image}}' immich_server 在每个转储旁记录版本,因为默认的 IMMICH_VERSION=v3 是浮动标签,无法提供任何有效版本信息。