背景

 

在之前的《用 Termux + Syncthing 自动定时备份 Android 媒体文件》一文中,我提到了自己采用 “Syncthing + Immich” 作为媒体文件的同步与管理方案。那篇文章中解决是的客户端的问题:如何将散落在手机上的媒体文件统一集中并备份到服务端。这篇文章则是对服务端应用的改造补充,讲述了我从 Nextcloud 到 Syncthing + Immich 的实践过程。

在开始之前,或许有必要说明一下我对 Nextcloud 有什么不满。客观来讲,作为一个私有云盘,Nextcloud 是一个非常合格的应用。我能想到的,它与之对标的闭源软件是 Microsoft 的 OneDrive。再加上 Nextcloud 的 App Store 中,官方和社区提供的丰富插件生态,使得用户完全可以以 Nextcloud 为中心,构建一个自己的 ALL IN ONE 工作台。 但是我使用 Nextcloud 的诉求,除了单纯的备份,还包括了图片的管理。固然在 Nextcloud 中提供了简单的图片浏览和管理功能,但比较专门的图片管理应用,它的体验只能说是“聊胜于无”,远谈不上优秀。因此,我希望寻找一个更加现代的相册管理应用。

方案比较

这里,有必要回顾一下当初的选型过程。Github 上,有一个仓库 对自托管的照片库进行了比较。严格来说,这里比较的都是 FOSS(Free and Open-Source Software,自由及开源软件)软件,所以自然不包括 MTPhotos 或群晖相册之类的商业方案。


从这里的功能比较中可以看到,Nextcloud 自带的照片功能,在 LivePhotos 和 Videos 的支持上的确较差,并且在 Albums、Timeline 和 Search 等照片展示和检索功能上也不尽如人意。
这里面我最关注的是两个应用,一个就是有着压倒性 stars 数量的 Immich,另一个就是与 Nextcloud 集成的 Nextcloud Memories。

Memories

先来说 Memories 方案,这是我的一开始尝试的方案。理由很简单,Memories 可以直接与 Nextcloud 服务集成,只需要在当前应用内部安装插件并进行配置就好了,不用引用额外的新服务。看起来很简单,但是在执行时还存在不少需要注意的地方,例如:

  • Nextcloud 的社区镜像 需要手动构建镜像才能支持硬件加速。
  • Memories 本身并不直接提供人脸识别功能,还需要依赖其他的插件,如 RecognizeFace Recognize 插件。
  • 针对中国大陆的网络环境需要对 Nextcloud 的 App Store 进行配置。

总得来说,Memories 仍然是 Nextcloud 的一个插件,并不是一个独立的应用服务。它的很多功能,如媒体转码、同步备份和人脸识别都与 Nextcloud 高度耦合。如果是重度依赖 Nextcloud 生态的用户,那么将 Memories 作为对 Nextcloud Photos 的增强替代,是非常合适的选择。无需单独再维护一套新的服务,只需要完成插件配置,就可以直接在原有的 Nextcloud 服务中,获得良好的照片管理体验。
但是对我来说,Nextcloud 的在线协作反而是多余的功能,所以我最终还是选择了单独部署图片服务的方案,Nextcloud 的部署配置就放在后文仅供参考了。

Immich

Immich 虽然被很多人视为“自托管版的 Google Photos”,但官网 的免责声明,即这段:

⚠️ The project is under very active development. Expect bugs and changes. Do not use it as the only way to store your photos and videos!”

让我一开始还是有些犯怵。但在 demo 环境进行尝试后,Immich 更现代的 UI 设计、更优秀的 AI 识别以及其他开箱即用的相册功能还是吸引了我。再加上它完善的官方文档和活跃的社区环境,最终促使我决定了这次迁移。
在方案设计上,虽然 Immich 安卓端提供了备份的功能,但通过 Immich 备份的相册目录是由应用自身来管理的,不能保留原本的文件结构。因此本着“专业的人干专业的事” 的原则,我将 Nextcloud 拆分为专门的同步应用 Syncthing 和专门的照片管理应用 Immich 来分别进行部署。

方案实施

部署采用的是 Docker Compose 方式,这也是 Immich 官方推荐的方式,在开始前,需要确保系统已经安装了 Docker 和 Docker Compose,并且有可以拉取镜像的网络环境。

准备工作

我当前对 docker 应用的数据是全部存放在 /docker 路径下,并以应用来划分的。以 Nextcloud 为例,一个 docker-compose.yml 文件中包含了应用(Nextcloud)、数据库(mysql)和缓存(redis),那么它的路径结构在宿主机上可能是这样的:

/docker/nextcloud/
├── app
│   ├── ...
│   └── nextcloud_data
├── db
│   ├── conf
│   └── data
└── cache
    └── data

在原本以应用为主的结构中,是不存在问题的,因为一个应用的数据只独属于这个应用容器。但是在现在拆分后方案中,媒体数据既需要 Syncthing 来进行同步,又需要在 Immich 中作为外部媒体库来进行管理,再按原来的方式进行处理就不是那么合适了。
因此,独立于应用的数据,我单独创建了一个 /data 路径专门来存放。例如这里 /data/pictures 就专门用来存放图片数据,/data/documents 则存放原本使用 Nextcloud 同步的文档数据。

数据迁移

有了上面的配置后,先进行的就是数据迁移工作。在磁盘有足够空间的情况下,这里可以先不用贸然将原来的数据直接移动(mv)到新路径,可以先复制一份保持冗余,等新应用稳定后,再进行归档或删除。
我先前的 Nextcloud 的文件存放在 /var/www/html/data/用户名/files/ 下,以我的配置文件为例,对应的宿主机路径为 /docker/nextcloud/app/data/用户名/files。这里只需要通过 rsync 迁移到新的目标路径即可:

# 归档模式进行复制
# 参数说明:
#    -a:归档模式,递归传输文件并保留所有文件属性
#    -v:详细模式,增加输出的详细程度
#    -P:显示进度并允许在传输中断后恢复部分传输的文件
#    -h:以人类易读的格式显示文件大小信息
# 如果有权限问题可能需要 sudo 执行
sudo rsync -avPh /docker/nextcloud/app/data/用户/files/pictures/ /data/pictures/

# 如果文件权限不一致(例如 nextcloud 中可能是 www-data 用户和用户组)执行
# sudo chown -R 1000:1000 /data/pictures

通常来讲,Linux 的第一个创建的用户的 UID 和 GID 都是 1000,因此后续都默认以 1000:1000 为例。也可以通过 id -uid -g 确认当前用户的 UID 和 GID。

Syncthing 配置

Syncthing 的部署本身是不复杂的,只是我针对自己的情况进行了一些改动:

services:
  syncthing:
    image: linuxserver/syncthing:2.0.3
    container_name: syncthing
    # linuxserver 的镜像支持 PUID 和 PGID 参数
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
    volumes:
    # 应用自身相关的配置,挂载在 /docker/syncthing 下
      - /docker/syncthing/config:/config
    # 外部数据单独挂载在 /data 路径下
      - /data/documents:/data/documents
      - /data/pictures:/data/pictures
      - /data/encrypted:/data/encrypted
    ports:
      - 8384:8384
      - 22000:22000/tcp
      - 22000:22000/udp
      - 21027:21027/udp
    restart: unless-stopped

然后启动 Syncthing 服务后就可以通过访问 http://<服务器IP>:8384,进行同步设备和同步文件夹的配置了。
Syncthing 默认是没有密码的,可以在 GUI 中设置身份验证,并且将默认监听的地址配置为局域网访问地址以增加安全性。Android 上使用 Syncthing-Fork 进行同步的话不需要额外进行这些操作,因为开发者已经进行了开箱即用的设置。

Immich 配置

Immich 的部署主要参考官方推荐的 Docker Compose。官方在 Github 仓库提供了文件,我只是针对自己使用外部媒体库的情况进行了一些小改动:

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
+   # 添加核显映射
+   # 核显设备路径为 /dev/dri/renderD128,用户组为 render
+   # 需要通过 cat /etc/group | grep render 来查看 render 组的 GID
+   user: "1000:1000"
+   group_add:
+     - "106"
+   devices:
+     - /dev/dri/renderD128:/dev/dri/renderD128 
    # extends:
    #   file: hwaccel.transcoding.yml
    #   service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
    volumes:
      # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
      - ${UPLOAD_LOCATION}:/data
+     # 中文地理位置编码配置
+     - /docker/immich/geodata:/build/geodata
+     - /docker/immich/i18n-iso-countries/langs:/usr/src/app/server/node_modules/i18n-iso-countries/lan
+     # 只读挂载外部媒体库
+     - /data/pictures:/data/pictures:ro
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database
    restart: always
    healthcheck:
      disable: false

  immich-machine-learning:
    container_name: immich_machine_learning
    # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag.
    # Example tag: ${IMMICH_VERSION:-release}-cuda
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
    #   file: hwaccel.ml.yml
    #   service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
    volumes:
-     - model-cache:/cache
+     - /docker/immich/cache:/cache
    env_file:
      - .env
    restart: always
    healthcheck:
      disable: false
+   # 对资源进行限制,避免占用过多宿主机资源
+   deploy:
+     resources:
+       limits:
+         cpus: '2.00'
+         memory: 6G
+       reservations:
+         cpus: '0.50'
+         memory: 4.5G   
# 其他部分保持不变

我对官方的 docker-compose 文件进行了一些改动,首先是映射了核显设备,使得 Immich 服务可以利用核显加速,另一个就是对机器学习的容器进行资源限制,避免占用宿主机过多资源。Immich 服务挂载的持久卷中,参考了 Immich 反向地理编码汉化 这个项目,以便为反向地理编码提供中文支持。配置成功后,在 Immich 的地图就可以看到的显示为中文的地理信息了。

注意, Immich 的部署需要准备两个文件,除了 docker-compose.yml,还需要下载 example.env 文件并更名为 .env 文件,并在其中对数据库密码等必要参数进行配置。

Immich 服务启动后,可以访问 http://<服务器IP>: 2283 ,创建并登录管理员账户。然后点击管理员账户下面的“系统管理”,进入管理页面配置外部图库,将挂载对应路径的外部图库进行导入,等待任务处理完成,刷新首页的时间线,应该可以看到外部图片成功展示。

Nextcloud 配置

仅作为参考:

  • 镜像补丁:Memories 支持视频和 Live Photos 的转码,以及核显的硬件加速。但这些功能并不是开箱即用的。如果和我一样,使用的是 Nextcloud 的社区镜像 来进行部署的话,这个镜像中并不包含核显的驱动以及 FFmpeg 转码工具,需要自行构建镜像(如果你不希望每次重建容器都执行安装命令的话)。一个示例的 Dockerfile 可能如下:
# 基础镜像
FROM nextcloud:stable

RUN \
    # 设置 DEBIAN_FRONTEND 为非交互模式,避免 apt-get 安装时卡住
    export DEBIAN_FRONTEND=noninteractive && \
    \
    # 1. 替换镜像源并添加 non-free 组件
    sed -i -e "s|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g" \
           -e "s|security.debian.org|mirrors.tuna.tsinghua.edu.cn/debian-security|g" \
           -e 's/ main$/ main contrib non-free non-free-firmware/g' \
           /etc/apt/sources.list.d/debian.sources && \
    \
    # 2. 更新并安装软件包
    apt-get update && \
    apt-get install -y --no-install-recommends \
        ffmpeg \
        intel-media-va-driver-non-free \
        vim && \
    \
    # 3. 进行清理
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

 

  • docker-compose.yml 配置:
services:
  app:
    # 使用重新构建后添加了核显渠道的镜像
    image: nextcloud:stable-patched
    container_name: nextcloud
    restart: unless-stopped
    # 如果使用非 root 用户,需要添加核显设备的用户组
    group_add:
      - "106"
    ports:
      - "18080:80"
    depends_on:
      - db
      - cache      
    environment:
      - TZ=Asia/Shanghai
      - PUID=1000
      - PGID=1000
      - MYSQL_HOST=db
    # 数据库配置,需要和下面 MySQL 中的配置保持一致
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=your_nextcloud_mysql_password
      - REDIS_HOST=cache
      - PHP_MEMORY_LIMIT=2048M
    volumes:
      - /docker/nextcloud/app:/var/www/html
    # 核显设备映射
    devices:
      - /dev/dri/renderD128:/dev/dri/renderD128  
    # 资源使用限制 
    deploy:
      resources:
        limits:
          cpus: '2.00'
          memory: 4G
        reservations:
          memory: 2G       
    networks:
      - nextcloud

  db:
    image: mysql:8
    container_name: nextcloud-db
    restart: unless-stopped
    environment:
      - TZ=Asia/Shanghai
      - PUID=1000
      - PGID=1000
      - MYSQL_ROOT_PASSWORD=your_mysql_root_password
    # 数据库配置,需要和上面 Nextcloud 中的配置保持一致
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=your_nextcloud_mysql_password    
    volumes:
      - /docker/nextcloud/db/data:/var/lib/mysql
      - /docker/nextcloud/db/conf:/etc/mysql/conf.d    
    networks:
      - nextcloud
  cache:
    image: redis:7
    container_name: nextcloud-cache
    restart: unless-stopped
    environment:
      - TZ=Asia/Shanghai
      - PUID=1000
      - PGID=1000
    volumes:
      - /docker/nextcloud/cache/data:/data
    command: redis-server --save 60 1 --loglevel warning
    networks:
      - nextcloud

networks:
  nextcloud:
    name: nextcloud
    external: false

优化配置

硬件加速

如果参考我的 Immich 配置了硬件加速,可以在“系统管理” -> “设置”中,拉到最下面的“视频转码设置”。在倒数第二个选项卡“硬件加速”,设置加速器 API 为 VAAPI,如果是 Intel 7 代以上的 CPU,还可以选择 Quick Sync 选项。

中文地图编码

参考项目 immich-geodata-cn 进行配置。在 Relases 中,下载和当前 Immich 一致的 geodata 文件,并解压到服务器指定路径,geodata 文件级别参考项目的说明。注意,因为 Immich 更新较快,适配最新版本的 Immich 的文件可能是自动更新版本。

机器学习设置

默认的模型是不支持中文的,如果希望语义搜索可以获得较好的效果,官方文档 提供了可选的多语言模型和它们的配置参考。比如,主要以中文检索的情况,可以选择 nllb-clip-large-siglip__v1 模型,它的内存要求是 4226 MB,这也就是我在前面的配置中,为 immich-machine-learning 预留了 4.5 G 内存的原因。如果服务器资源拮据,也可以考虑使用默认的模型或者关闭机器学习功能。
同样的,需要在“系统管理” -> “设置”中找到“机器学习设置”中进行配置,这里只需要将智能搜索选项卡中的 CLIP 模型更换为选择的模型即可,immich-machine-learning 服务会自动进行模型下载。如果遇到网络问题,可以安装 gitlfs 插件,通过镜像网站手动拉取模型到本地。同样以我的配置为例,本地的模型路径为 /docker/immich/cache/clip。因此只需要在这个路径下执行 git clone https://hf-mirror.com/immich-app/nllb-clip-large-siglip__v1 即可。

写在最后

如果设备的性能不是太好,Immich 的 AI 识别可能会运行较长的时间。以我的 12300T,近 1.5 万张照片和图片,运行了大约 8 个多小时。对移动设备上的照片备份,我也额外设置了同步脚本的配置。
通过拆分,我获得了什么?在告别了对我而言稍嫌鸡肋的 Nextcloud 后,我获得了一个更加现代的照片管理应用和一个更加纯粹的同步应用。我不再试图在一个“大而全”的应用中解决全部问题,而是让不同的应用各司其职,最终组合为一个协调配合的系统。这大概就是自托管的魅力。

6
3