群晖照片异地备份到 Unraid + Immich 相册搭建详细教程:rsync 同步与踩坑全记录

我家里同时有白群晖和 unRAID 两台 NAS ,准备把群晖里攒了几年的照片进行异地备份,单盘 NAS 一旦坏了,照片可就全没了。 我自己折腾了两天,踩了一堆坑,终于跑通了一套「unRAID 定时拉取群晖 + Immich 相册」的方案。今天把完整过程整理出来,从组网到备份再到相册,新手照着走也能一步到位。
零、开始前的准备
准备项目
| 类别 | 内容 |
|---|---|
| 硬件 | 一台群晖(数据源)、一台 unRAID(备份端,强烈建议带一块 SSD 做 cache 池) |
| 账号 | 无需注册(EasyTier 去中心化,组网密钥一致即可) |
| 前置知识 | 会用 SSH 登录、懂一点 Docker/Compose 概念 |
本文示例环境
| 机器 | 系统 | 硬件 | 角色 |
|---|---|---|---|
| 白群晖 | DSM 7.x | 存有照片/资料 | 数据源 |
| unRAID | unRAID 6/7 | i5-12400 / 32G / 16T 机械盘 + 一块 SSD | 备份端 + 相册 |
一、这套方案解决什么问题
先说需求:我有一台白群晖(存了几年照片和资料,是数据源),一台 unRAID 服务器(i5-12400 + 32G 内存 + 16T 机械盘 + 一块 SSD),两台机器不在同一个物理网络。目标是:
-
把群晖的照片、资料异地备份到 unRAID 的 16T 盘上;
-
在 unRAID 上搭一个** Immich 相册库**,方便随时翻照片。
一开始最自然的想法是「群晖用 Hyper Backup 主动推到 unRAID」,但实操下来发现这条路坑特别多,最后改成了「unRAID 主动拉取」,反而更稳。这篇文章就是这套最终方案的完整教程。
二、整体架构一览
整个流程就三步:
-
组网:用 EasyTier 把两台机器打通(去中心化虚拟局域网,P2P 直连);
-
备份:unRAID 定时用 SSH + rsync 把群晖照片「拉」到本地 16T 盘;
-
相册:Immich 以「外部库」方式挂载拉下来的照片,只建索引、不重复占空间。
涉及到的软硬件清单:
| 角色 | 设备 | 系统 | 关键配置 |
|---|---|---|---|
| 数据源 | 白群晖 | DSM 7.x | EasyTier(套件)、SSH、rsync 服务 |
| 备份端 | unRAID | unRAID 6/7 | EasyTier(Docker)、Immich(Docker Compose) |
三、EasyTier 组网
两台机器跨公网,先得让它们互相能访问。我用的是 EasyTier。安装可见我之前写的一篇文章EasyTier 私有虚拟网络搭建全流程 - 阿雷的小窝
EasyTier 组好网后,出现了一个诡异的现象:从群晖主动连 unRAID 时,ping 通、但 TCP 连不上。
ping -c 3 10.x.x.4 # 3/3 通,11ms ✅timeout 5 bash -c 'cat < /dev/null > /dev/tcp/10.126.126.4/873' && echo "通" || echo "不通"
查了半天,靠 easytier-cli peer 揪出规律:EasyTier 2.6.4 在「群晖出站」方向上的 TCP 有问题——群晖主动发起的 TCP、目标也是 2.6.4 节点时会不通;但反过来(unRAID 主动连群晖)完全正常。
也就是说:
-
❌ 群晖主动连 unRAID(推模式)→ 不行
-
✅ unRAID 主动连群晖(拉模式)→ 正常

这个「ping 通、TCP 断」的判断过程,浓缩成一张决策树(排查时照着走,少绕弯):
搞了半天没解决,决定不跟这个 bug 死磕,直接改用拉模式(unRAID 主动 SSH 到群晖),方向反过来就绕开了。
四、群晖端准备
4.1 先开三个开关(缺一不可)
| 位置 | 操作 |
|---|---|
| 控制面板 → 终端机和 SNMP → 终端机 | 勾选「启用 SSH 服务」 |
| 控制面板 → 文件服务 → rsync | 勾选「启动 rsync 服务」 |
| 控制面板 → 用户与群组 → 高级 | 勾选「启用家目录服务」 |
这三条是 DSM 的硬约束:
-
SSH 登录只对
administrators组开放——普通用户 shell 是/sbin/nologin,连上直接Permission denied; -
不开家目录服务,
authorized_keys无处安放,报Could not chdir to home directory; -
不开 rsync 服务,即使走 SSH 也会报
rsync error: service disabled (code 52)。

4.2 建一个最小权限的备份账号
在「控制面板 → 用户与群组」新建账号 backup,关键一步:把它加入 administrators 组(对应上面的硬约束 1)。有效权限再用三层锁住:共享文件夹只读、应用权限只放 rsync、SSH 来源限定。
4.3 最坑的一个:DSM 的 ACL 覆盖了 chmod
账号建好后,部署公钥时大概率会卡在权限上——即使你把 authorized_keys 设成了 600,sshd 日志里还是报:
Authentication refused: bad ACL permission for file /volume1/homes/backup/.ssh/authorized_keys根因:DSM 家目录 drwx--x--x+ 那个 + 是 synoacltool 挂的扩展 ACL,DSM 给 sshd 打了 patch 专门检查它,太宽就报 bad ACL permission。而 chmod 只改传统权限位,动不了 ACL,所以怎么 chmod 都没用。
正确解法是用 synoacltool -del 删掉 ACL,再 chmod:
synoacltool -del /volume1/homes/backup/.ssh/authorized_keyssynoacltool -del /volume1/homes/backup/.sshsynoacltool -del /volume1/homes/backupchmod 755 /volume1/homes/backupchmod 700 /volume1/homes/backup/.sshchmod 600 /volume1/homes/backup/.ssh/authorized_keyschown -R backup:users /volume1/homes/backup/.ssh跑完验证一下,+ 号应该消失:
ls -la /volume1/homes/backup/.ssh/ # authorized_keys 应为 -rw-------,且无 + 号五、unRAID 端配置免密拉取
群晖端就绪后,回到 unRAID 配置 SSH 免密和拉取脚本。
5.1 生成密钥(注意路径)
mkdir -p /boot/config/sshssh-keygen -t ed25519 -N "" -f /boot/config/ssh/syno_pull为什么放
/boot/config/ssh? unRAID 的/root是内存盘,重启就清空,密钥放那儿会丢。/boot是 U 盘,能持久化。
5.2 把公钥部署到群晖
ssh-copy-id -i /boot/config/ssh/syno_pull.pub backup@10.x.x.20(如果报了 ACL 相关的错,就是 4.3 那步没处理干净,回去先删 ACL。)
5.3 密钥持久化(关键一步)
/boot 是 FAT32,权限位不持久,私钥直接放上面会是 777,被 OpenSSH 以「权限过宽」拒绝。所以正确姿势是「U 盘存源 + 开机复制到内存 + chmod」。
先装 User Scripts 插件(如果没有):
-
Unraid 网页 → Apps(Community Applications) → 搜索
User Scripts→ 安装; -
装完左侧导航栏会出现 User Scripts 入口。

然后在 User Scripts 里新建脚本(点 ADD NEW SCRIPT),运行时机选 At Startup of Array:
#!/bin/bashmkdir -p /root/.sshcp /boot/config/ssh/syno_pull /root/.ssh/syno_pullchmod 600 /root/.ssh/syno_pullssh-keyscan 10.x.x.20 >> /root/.ssh/known_hosts 2>/dev/null5.4 验证免密登录
ssh -i /root/.ssh/syno_pull backup@10.x.x.20 "whoami"成功的标志:直接输出 backup、不提示密码:
backup如果还提示 backup@10.x.x.20's password:,说明公钥没被接受,回头查 4.3 的 ACL。

六、正式备份与定时
6.1 先确认群晖上的备份源路径
命令里的 /volume1/photo/ 只是示例,你要换成自己群晖上真实的共享文件夹名。先在群晖 SSH 里看:
ls /volume1/会列出所有共享文件夹,比如 photo、video、homes、docker 等,记下你要备份的那个名字,路径就是 /volume1/<文件夹名>/。
如果你想备份的是群晖「家目录」里的照片(Synology Photos 默认存这里),路径通常是
/volume1/homes/<用户名>/Photos。
6.2 先 dry-run 空跑一遍
正式拉之前,务必先空跑,确认文件量和权限都对:
rsync -av --dry-run --stats \ --exclude '@eaDir' --exclude '#recycle' --exclude '@tmp' \ --exclude '@snapshot' --exclude '.DS_Store' --exclude 'Thumbs.db' \ -e "ssh -i /root/.ssh/syno_pull -o BatchMode=yes" \ backup@10.x.x.20:/volume1/photo/ /mnt/user/backup-photos/ 2>&1 | tail -10参数逐项解释(知道含义才不会瞎改):
| 参数 | 作用 |
|---|---|
-a | 归档模式(保留权限、时间戳等),等价 -rlptgoD |
-v | 显示详细输出 |
--dry-run | 只模拟,不实际传文件(空跑) |
--stats | 末尾输出统计信息(文件数、大小) |
--exclude | 排除指定目录/文件,可多次 |
-e "ssh -i ..." | 指定走 SSH,并用指定私钥 |
--partial(正式跑才加) | 断点续传,中断重跑不从头来 |
--info=progress2(正式跑才加) | 显示总体进度条 |
--exclude '@eaDir'一定要加:那是群晖生成的缩略图/索引目录,不排除会白拉几十万张缩略图、还干扰 Immich 扫描。#recycle、@tmp、@snapshot同理,都是群晖的系统目录。
看末尾的 Number of files 和 Total file size,确认数量合理、没有一堆 Permission denied,就可以正式跑了。
成功的标志(实测输出示例,数字以你自己的为准):
Number of files: 34,713 (reg: 28,878, dir: 5,725, link: 110)Total file size: 53,988,164,580 bytes-
文件数、总大小和你群晖上对得上 → 数量没问题;
-
没有刷屏的
Permission denied→ 权限没问题; -
然后去掉
--dry-run正式拉。
注意:上面这个数字里
@eaDir还没排除时会虚高(缩略图也算进去了),正式跑带上--exclude '@eaDir'后,真实照片数会少一大截,这是正常的。
6.3 正式拉取
去掉 --dry-run,加上断点续传:
rsync -av --partial --info=progress2 \ --exclude '@eaDir' --exclude '#recycle' --exclude '@tmp' \ --exclude '@snapshot' --exclude '.DS_Store' --exclude 'Thumbs.db' \ -e "ssh -i /root/.ssh/syno_pull -o BatchMode=yes" \ backup@10.x.x.20:/volume1/photo/ /mnt/user/backup-photos/
6.4定时自动备份
在 User Scripts 里把上面的命令包装成脚本,运行时机选 Scheduled(比如每天凌晨 3 点)。首次跑完,之后就是增量同步,只传改动的文件。
七、Immich 搭建与照片管理
照片备份到本地后,用 Immich 做相册。这一步有个致命坑必须避开,否则重启就丢数据。
7.1 用 Docker Compose 部署(完整配置)
先装 Docker Compose Manager 插件(如果还没有):
-
Unraid 网页 → Apps(Community Applications) → 搜索
docker compose manager→ 安装; -
装完在 插件 页面能看到它,点开就是 Compose 管理界面。
新建 stack:点 ADD NEW STACK → 名字填 immich → 点进齿轮 → EDIT STACK → 选 COMPOSE FILE,把下面这份 完整可用的 docker-compose.yml(按需进行修改)粘贴进去:
name: immich
services: immich-server: container_name: immich_server image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} volumes: - ${UPLOAD_LOCATION}:/data # 上传目录(在 .env 里定义) - /etc/localtime:/etc/localtime:ro - /mnt/user/backup-photos:/mnt/nas_photos:ro # 外部库:挂 rsync 同步下来的照片 env_file: - .env ports: - '2283:2283' depends_on: - redis - database restart: always
immich-machine-learning: container_name: immich_machine_learning image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} # 想用核显加速(i5-12400 有 UHD 730),改成下面三行,见 7.5: # image: ghcr.io/immich-app/immich-machine-learning:release-openvino # devices: [ "/dev/dri:/dev/dri" ] # device_cgroup_rules: [ 'c 189:* rmw' ] volumes: - model-cache:/cache env_file: - .env restart: always
redis: container_name: immich_redis image: docker.io/valkey/valkey:9 volumes: - /mnt/user/appdata/immich/redis:/data # redis 持久化(否则重启丢任务队列) 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 # 必须落 SSD,见 7.2 的坑 shm_size: 128mb restart: always
volumes: model-cache:配套的 .env:
# 上传照片的存储位置(unRAID 的用户共享路径)UPLOAD_LOCATION=/mnt/user/appdata/immich/photo/
# 数据库存储位置:必须是 SSD cache 盘(见 7.2 的坑)DB_DATA_LOCATION=/mnt/cache/appdata/immich/db
# Immich 版本(固定主版本,避免滚动升级出兼容问题)IMMICH_VERSION=v3
# postgres 密码:改成你自己的强密码DB_PASSWORD=your-strong-passwordDB_USERNAME=postgresDB_DATABASE_NAME=immich
# 国内必加:模型下载走 hf-mirror 镜像(见 7.4.1)HF_ENDPOINT=https://hf-mirror.com几个要点:
-
数据库路径用变量
DB_DATA_LOCATION,统一在.env里改,别在 compose 里硬编码(避免.env和 compose 两处路径不一致的坑); -
外部库挂载
/mnt/user/backup-photos:/mnt/nas_photos:ro,把 rsync 拉下来的照片只读挂进容器; -
HF_ENDPOINT国内必加; -
IMMICH_VERSION固定主版本(如v3),不要用会滚动的release,避免升级踩兼容坑。
启动:填完 compose 和 env,回到 stack 页面点 COMPOSE UP,等镜像拉取完成(第一次会拉几个 GB)。
首次初始化(第一次打开):
-
浏览器访问
http://unraid-ip:2283; -
第一次会进入 创建管理员 页面,填邮箱 + 密码(这就是你的 Immich 管理员账号,记牢);
-
进入后就是正式界面了。

7.2 postgres 必须落在真实 SSD 上
compose.yml 里 database 的挂载要特别注意:
database: volumes: - /mnt/cache/appdata/immich/db:/var/lib/postgresql/data问题出在 /mnt/cache 这个路径:它是 unRAID 的 cache 池(SSD)挂载点。如果你没配 SSD cache 盘,这个目录根本不存在——Docker 挂载不存在的路径时会自动创建它,创建在 unRAID 的根文件系统里,而根文件系统是内存。于是 postgres 数据全写进内存,一重启就清零,Immich 就显示「重新安装」,人脸识别、标签全没了。
修复:给 unRAID 加一块 SSD 做 cache 池(postgres 本就该在 SSD),或退而求其次把 DB 落到阵列持久化目录。核心原则:postgres 数据必须落在一个真实、持久的磁盘上。
7.3 用「外部库」管理照片
照片已经在 /mnt/user/backup-photos/ 了,Immich 不用重新上传,直接挂成外部库只做索引。在 compose.yml 的 immich-server 服务里加一个只读挂载:
immich-server: volumes: - /mnt/user/backup-photos:/mnt/nas_photos:ro改完 docker compose up -d 重启,然后在 Immich 后台:管理 → 外部库 → 创建外部库 → 导入路径填 /mnt/nas_photos。

外部库是只读索引:Immich 不改动照片原件,只建数据库索引。照片在
/mnt/user/backup-photos/,索引在 postgres——这也再次说明 postgres 必须持久化,否则索引每次重建。
7.4 开启人脸识别
人脸识别默认就是开启的,由 immich-machine-learning 容器承担,不需要改任何配置。但外部库的照片不会自动全部处理,需要手动触发,而且顺序很关键:先检测、后识别。
-
先扫描外部库:管理 → 外部库 → 你的库 → 扫描(Scan),让照片进库;
-
管理 → 任务(Jobs)→ 人脸检测(Face Detection),点右侧 全部(All);
-
等检测跑完(Jobs 页有进度条),再点 人脸识别(Facial Recognition) 的 全部(All);
-
全部跑完后,到 探索(Explore)→ 人物(People),给聚类出的人脸命名,之后 Immich 会自动归类同名的人。

为什么「人脸识别显示 0」? 因为人脸识别依赖人脸检测先跑——检测没跑,识别队列永远是 0;而照片没 scan 进库,检测又无从谈起。所以卡在 0 时,从「外部库有没有扫描」往上查。
7.4.1 国内必踩的坑:模型下载失败
人脸识别依赖的 buffalo_l 模型(以及智能搜索的 CLIP 模型)默认从 HuggingFace 下载,国内直连不通,会出现典型的死循环:
Downloading detection model 'buffalo_l' ... This may take a while.Failed to load detection model 'buffalo_l'. Clearing cache.Attempted to clear cache for model 'buffalo_l', but cache directory does not exist解决:在 .env 里加一行,把模型下载源切到国内镜像:
HF_ENDPOINT=https://hf-mirror.com然后重启 ML 容器:
docker compose up -d immich-machine-learning重启后再看日志,出现 Fetching ... 100% 和 Loading detection model 'buffalo_l' to memory 就说明下载成功、可以正常识别了。
成功的标志(加了 HF_ENDPOINT 后,日志从「下载失败」变成「下载成功」):
Downloading detection model 'buffalo_l' ... This may take a while.Fetching 4 files: 100%|██████████| 4/4 [00:06<00:00, 1.60s/it]Loading detection model 'buffalo_l' to memoryLoading recognition model 'buffalo_l' to memory关键看 Fetching ... 100%(下载成功)和 Loading ... to memory(加载成功)——之前失败时是 Failed to load ... Clearing cache。
7.5 硬件加速(可选):用核显 OpenVINO
默认用 CPU 跑,几万张照片会偏慢。i5-12400 自带 UHD 730 核显,可改成 OpenVINO 加速——前提是核显没被其他虚拟机直通占用。
先确认核显可用:
ls /dev/dri # 能看到 renderD128 之类即说明核显可用改 compose(⚠️ unRAID 上 device_cgroup_rules 这行必须加,否则容器访问不到 /dev/dri):
immich-machine-learning: image: ghcr.io/immich-app/immich-machine-learning:release-openvino devices: - /dev/dri:/dev/dri device_cgroup_rules: - 'c 189:* rmw'改完拉新镜像并重启:
docker compose pull immich-machine-learningdocker compose up -d immich-machine-learning怎么确认用上了核显? 看 ML 日志的 execution provider:
docker logs immich_machine_learning 2>&1 | grep -i "execution provider"-
CPUExecutionProvider→ 还在用 CPU -
OpenVINOExecutionProvider→ 核显生效 ✅

成功的标志:日志里 OpenVINOExecutionProvider 排在第一位(CPU 只作为 fallback 排第二),说明核显已接管推理:
Setting execution providers to ['OpenVINOExecutionProvider', 'CPUExecutionProvider'], in descending order of preference对比改造前(纯 CPU):['CPUExecutionProvider'] —— 前后一字之差,就是核显是否生效的分水岭。
device_cgroup_rules: 'c 189:* rmw'是给容器开放/dev/dri(字符设备 189)的读写权限。unRAID 的 Docker 默认 cgroup 限制较严,漏了这行即使挂载了/dev/dri也访问不到——这是 unRAID 上特有的坑。
7.6 其他几个实用配置
- redis 持久化:官方 compose 的 redis 默认没挂数据卷,重启会丢任务队列,建议加一行:
redis: volumes: - /mnt/user/appdata/immich/redis:/data-
最小识别人脸数:默认
3,即同一个人至少要出现 3 张脸才会被建为「人物」。若某人照片少被隐藏,可在 管理 → 设置 → 机器学习 → 人脸识别 里调低。 -
改参数 vs 换模型:只改检测阈值,重跑「人脸识别-全部」即可;换了识别模型,则要重跑「人脸检测-全部」。
-
双胞胎/相似脸:可在人脸识别设置里调低「最大识别距离」来区分。
7.7 全功能模型清单:env 不用加任何模型配置
Immich 的机器学习功能默认全部开启,模型会自动下载,不需要在 .env 里写模型名(唯一要加的就是 7.4.1 的 HF_ENDPOINT)。各功能对应模型:
| 功能 | 模型 | 需要动吗 |
|---|---|---|
| 人脸识别 | buffalo_l | ❌ 默认够用 |
| 智能搜索(自然语言搜图) | ViT-B-32__openai(默认) | ⚠️ 中文建议换 |
| OCR 文字识别 | PP-OCRv5_mobile | ❌ 自动加载 |
| 重复检测 | 复用 CLIP | ❌ |
| 反向地理编码 | GeoNames 库(非 ML) | ❌ |
唯一建议动的:中文智能搜索换模型。默认 ViT-B-32__openai 偏英文,中文搜图(如「海边的日落」「春节鞭炮」)效果差,建议换成多语言的 XLM-Roberta 系列。
操作(在 UI 改,不是 env):管理 → 设置 → 机器学习设置 → 智能搜索 → 模型名称里粘贴:
XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k保存后到 Jobs 页点「智能搜索 → 全部」重新建索引。内存够可上更准的 XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k,但更吃内存(32G 还要跑 Windows VM 的话,先用 Base 版)。
⚠️ 网上常见的
CLIP_VISUAL_MODEL_NAME等 env 变量是 MT Photos(另一个相册软件)的,不是 Immich。Immich 的模型一律在后台 UI 里改。
八、常见问题排查(FAQ)
| 问题 | 原因 | 解决 |
|---|---|---|
| 群晖上 telnet 命令不存在 | DSM 没有 telnet 客户端 | 用 bash -c 'cat < /dev/null > /dev/tcp/ip/port' 测端口 |
| 测端口显示”不通”其实是通的 | cat </dev/tcp 会等 EOF 被 timeout 误杀 | 换成 cat < /dev/null > /dev/tcp/... |
SSH 免密一直 Permission denied | DSM 家目录 ACL 覆盖了 chmod | 用 synoacltool -del 删 ACL,见 4.3 |
SSH 连上但 Could not chdir to home | 家目录服务没开 | 用户与群组 → 高级 → 启用家目录服务 |
rsync 报 service disabled (code 52) | rsync 服务没开 | 文件服务 → rsync → 启用 |
| Immich 重启显示”重新安装” | postgres 挂到了 /mnt/cache(无 SSD 时落内存) | DB 改到真实持久盘,见 7.2 |
| 人脸识别显示 0 | 照片没 scan 进库,或人脸检测没先跑 | 先扫描外部库,再「先检测后识别」,见 7.4 |
日志报 Failed to load model / 人脸检测一直失败 | 模型从 HuggingFace 下载失败(国内不通) | .env 加 HF_ENDPOINT=https://hf-mirror.com,见 7.4.1 |
| 人脸识别跑完但 People 里人很少 | 最小识别人脸数默认 3,人出现不足 3 张被隐藏 | 调低最小识别人脸数,见 7.6 |
备份读到 Permission denied | backup 账号读不到别人的文件 | 群晖配 sudoers + --rsync-path="sudo rsync" |
docker compose up 报 Invalid container name | 把镜像名写进了 container_name 字段(镜像名含 / 和 :,不能当容器名) | 检查 compose:container_name 只填容器名,image 才填 ghcr.io/... 镜像名,见 7.1 |
总结
折腾了这么一圈,总算把「群晖相册 → unRAID 备份」这条路走通了。最大的意外收获是 Immich——它有全平台客户端,用下来比 Synology Photos(DS Photos)顺手不少,尤其是后者看照片地图时经常崩溃的问题,在 Immich 这边完全不存在。
所以我现在是这么分工的:上传和同步交给 Synology Photos,白嫖它的 QuickConnect 远程能力;照片的处理、查看交给 Immich,人脸识别、智能搜索都在这边跑。两者各干各擅长的,互补着用。
整套方案从组网到备份再到相册搭建,全部配置和踩过的坑都写在上面了,照着做基本能一次到位。如果你也有异地备份照片的需求,不妨试试这套组合,应该不会让你失望。
参考
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!


