中文 English

PhotoPrism 启动 5 分钟还在装系统包?一次 PHOTOPRISM_INIT=intel 的踩坑与正确配置

发布时间: 2026-06-13
PhotoPrism Docker NAS Synology s6-overlay Intel VAAPI QSV 家庭实验室 故障排查

TL;DR(先说结论)

PhotoPrism Plus 镜像的 docker-compose.yml 里有一行 PHOTOPRISM_INIT: "intel",看似只是给 Intel 核显开加速,实际上它在容器第一次启动时会去 archive.ubuntu.com 跑一次完整的 apt-get dist-upgrade,再装 7 个 GPU / VA-API 包。这套流程 5–10 分钟起步,整个期间容器内部没有任何进程在监听 2342 端口。你的 docker ps 显示 Up、Portainer 一切绿、ss -ltn 也看到 0.0.0.0:2342 在 LISTEN——但浏览器一访问就是 ERR_CONNECTION_RESET / Connection reset by peer

解法非常简单:删掉或清空那一行。你 devices 里已经挂上 /dev/dri/renderD128 直通,驱动和 VA-API 用户态工具由宿主机提供,容器里再装一遍是纯浪费。

PhotoPrism 跑起来后的样子(官方截图)

封面:本文主角 PhotoPrism 正常运行时的样子——如果你看到的是这个画面,说明你一切正常;如果看到的是 Connection reset by peer,请继续往下读。


问题背景:一台 NAS 跑 PhotoPrism 的"标准姿势"

我家里有一台基于 Synology DSM 的 NAS,平时跑几十个 Docker 容器:Portainer、Syncthing、qbittorrent、Jellyfin、Photoprism、photoprism-ro …… 全都是用 Portainer 的 stack 部署。

PhotoPrism 是两个实例同时跑:

两者的 docker-compose.yml 都长得几乎一样,最关键的"差异化"那行长这样:

environment:
  PHOTOPRISM_INIT: "intel"

加上这一行是为了让 PhotoPrism 在容器里用上 Intel 核显做 QSV 硬解——毕竟 PhotoPrism 跑视频转码的时候,软件转码的 CPU 占用会爆。

一切看上去都挺合理的,对吧。


问题表现:容器是 Up 的,但访问就是连不上

部署完 stack 之后,我去 Portainer 看了一眼——

在主机上 ss -ltn

LISTEN  0  128  0.0.0.0:2342  0.0.0.0:*  users:(("docker-proxy",pid=5867,fd=4))
LISTEN  0  128  [::]:2342     [::]:*     users:(("docker-proxy",pid=5873,fd=4))

一切看起来很完美——端口在 LISTEN,docker-proxy 已经在等连接了。

但我打开浏览器访问 http://<NAS>:2342/,Chrome 直接给我一个 ERR_CONNECTION_RESET。换 Safari 也一样。换 curl

$ curl -v http://<NAS>:2342/
> GET / HTTP/1.1
> Host: <NAS>:2342
> User-Agent: curl/8.7.1
> 
* Request completely sent off
* Recv failure: Connection reset by peer
* Closing connection

Connection reset by peer,RST。这是 TCP 层就被对端掐了。

我又去看 Portainer 那个 stack 的状态:绿点,healthy

这就让人非常困惑:容器到底起没起来?


问题分析:把"Up"三个字拆开看

我当时第一反应是"容器没起来",于是去主机上 docker inspect,看到 Status=running,也就没再多想。但事实上,Up 跟"服务可用"是两件完全不同的事

我钻进容器内看了一眼:

$ docker exec photoprism ps -ef
UID   PID  PPID  C STIME  CMD
root    1     0  0 23:28  /package/admin/s6/command/s6-svscan -d4 -- /run/service
root   17     1  0 23:30  s6-supervise s6-linux-init-shutdownd
root   18    17  0 23:30  /package/admin/s6-linux-init/...shutdownd
root   28     1  0 23:30  s6-supervise s6rc-oneshot-runner
root   29     1  0 23:30  s6-supervise s6rc-fdholder
root   30     1  0 23:30  s6-supervise photoprism
root   36    28  0 23:30  s6-rc-oneshot-run
root   60    30  0 23:30  bash /scripts/cmd.sh /opt/photoprism/bin/photoprism start
root   63    60  0 23:30  bash /scripts/entrypoint-init.sh
root   65    63  0 23:30  make --no-print-directory -C /scripts intel
root  106    65  0 23:31  apt-get -qq dist-upgrade

看到了吗?1 号进程是 s6-svscan,它才是 docker 视角里"Up"的那个。但 1 号进程下面"实际干活"的是 entrypoint-init.shmake -C /scripts intelapt-get -qq dist-upgrade

更关键的是:photoprism 主进程根本没起来。它被困在 s6-supervise photoprism 的等待队列里——/scripts/cmd.sh 必须等 entrypoint-init.sh 跑完,才会执行真正的 photoprism start

再看容器内部有没有人在监听 2342:

$ docker exec photoprism ss -ltn
# (空——什么都没人 listen)

$ docker exec photoprism bash -c 'for p in 2342 3000; do
    timeout 1 bash -c "</dev/tcp/127.0.0.1/$p" && echo $p OPEN || echo $p CLOSED
  done'
2342 OPEN    # 这是从容器内看的 docker-proxy 转发,不算
3000 CLOSED  # 真正的 app 监听端口

真相浮出水面:

检查位置 结果 含义
docker ps (host) Up 3 minutes s6 监督树在跑 ✅
ss -ltn (host) 0.0.0.0:2342 LISTEN docker-proxy 在等 ✅
curl http://<NAS>:2342/ Connection reset by peer 应用没起,proxy 没法转发 ❌
docker exec ... ps -ef 卡在 apt-get dist-upgrade init 阶段还没完 ❌
docker exec ... ss -ltn 容器内 app 进程没起 ❌

三种"Up"含义在这里交汇——只有"容器内 app 进程在监听"才代表服务真的可用


s6-overlay 在 init 阶段里堵死的具体路径

图 1:PhotoPrism 镜像的 s6-overlay 启动路径。s6-svscan 是 PID 1,但它只负责监督;真正负责"初始化"的是 stage 2 的 s6rc-oneshot-runner,它会先跑 entrypoint-init.sh,而 PHOTOPRISM_INIT=intel 会让这个脚本去调 make -C /scripts intel,进而执行 apt-get dist-upgrade + 装 7 个 GPU 包。photoprism start 主进程被卡在这一步后面,整个期间 2342 没人监听


问题根因:PHOTOPRISM_INIT=intel 是个隐形的"装系统"开关

要理解为什么 PHOTOPRISM_INIT=intel 会卡这么久,先要搞清楚它的语义。

PhotoPrism 官方文档PHOTOPRISM_INIT 的描述非常简洁:

Run/install on first startup. Common options: update tensorflow https intel gpu davfs yt-dlp.

只有一行,完全不告诉你 intel 到底装了什么、装多久、装在哪。点进去 Makefile 才会看到真相——PhotoPrism 官方 Makefileintel 目标实际展开为:

intel: update install-intel
update:
	apt-get update
	apt-get -qq dist-upgrade      # ← 整系统升级
install-intel:
	@echo "Installing Intel GPU Drivers..."
	apt-get -qq install \
	  intel-opencl-icd \
	  intel-media-va-driver-non-free \
	  i965-va-driver-shaders \
	  mesa-va-drivers \
	  libmfx-gen1.2 \
	  va-driver-all \
	  vainfo \
	  libva2                       # ← 8 个包

而镜像里的 entrypoint-init.sh 又是这样调用的:

make --no-print-directory -C /scripts intel
# 展开为:
# apt-get update
# apt-get -qq dist-upgrade    (拉 archive.ubuntu.com 25MB+ 索引,全量 dist-upgrade)
# apt-get install 8 个 intel/va-api 包

而这一切全部发生在 photoprism 主进程启动之前

我当时截到的现场日志(脱敏后):

apt-get -qq dist-upgrade
Get:1 http://archive.ubuntu.com/ubuntu plucky InRelease [265 kB]
...
Fetched 25.2 MB in 16s (1571 kB/s)
...
Preparing to unpack .../systemd_257.4-1ubuntu3.2_amd64.deb ...
Preparing to unpack .../util-linux_2.40.2-14ubuntu1.2_amd64.deb ...
Setting up libc6:amd64 (2.41-6ubuntu1.2) ...

apt 在解包 systemd、libc6、util-linux、linux-libc-dev 等等——这不是"装个 Intel 驱动",这是给容器做一次完整的 OS 升级

我当时看到的进程状态:

PID   ELAPSED  CMD
106   02:45    apt-get -qq dist-upgrade

整整 2 分 45 秒过去了,还在解包 dpkg


主机、proxy、容器内三层"状态"的对照

图 2:同一个时刻,主机上docker-proxy容器内 三层"Up"的语义对比。Portainer 的绿点只反映最外层——s6-svscan 进程没死,仅此而已。要判断服务是否真的可用,必须进到容器里看 ss -ltnps -ef


为什么 curl 拿到的是 RST?

在搞清楚根因之后,还要解释一个看似更奇怪的细节:为什么 ss -ltn 在主机上明明显示 :2342 在 LISTEN,curl 过去却被对端 RST 回来?

这是 Docker 的 docker-proxy(也叫做 dockerd-proxy)的标准行为。

当你 docker run -p 2342:2342 的时候,docker 会在主机上启动一个 docker-proxy 进程,它的工作就是:

  1. 在主机上监听 0.0.0.0:2342(这就是你 ss -ltn 看到的那行 LISTEN)
  2. 接受 TCP 连接
  3. 把连接转发到容器的 network namespace127.0.0.1:2342

关键来了:转发是逐连接的。每次一个新连接到来,docker-proxy 都会去容器的 NS 里面尝试 connect(127.0.0.1:2342)。如果容器里面有人 listen 这个端口——握手完成、数据顺畅转发。如果没人 listen——容器内核回 RST,docker-proxy 拿到 RST 之后把 RST 透传给客户端。

而你完全无法在主机上区分"proxy 没人可连"和"应用在思考"——proxy 永远在 LISTEN。这正是这个错误的迷惑性所在。

用伪代码表示就是:

client → SYN → docker-proxy
docker-proxy → SYN → container_NS:2342   # 这里没人 LISTEN
container_NS → RST → docker-proxy        # 内核回 RST
docker-proxy → RST → client              # 透传
client: Connection reset by peer

事后docker exec 进容器、再等 8–10 分钟,init 跑完之后再看,里面的 2342 自然就有人监听了:

$ curl http://<NAS>:2342/
< HTTP/1.1 307 Temporary Redirect
< Location: /library/login

正常的 307 重定向到登录页。问题不是"PhotoPrism 起不来",而是"你以为它没起"


同一个 curl 在主机上 RST,docker exec 进去看真相

图 3:左图:浏览器 / curl 从主机外部访问 :2342,拿到 RST;右图:docker exec photoprism ps -ef 看到真相——主进程被 apt-get dist-upgrade 卡住,连 listen 都没起来。


一个 5 分钟 init 阶段的"占位图":里面在干什么,外面看起来像什么

图 4:典型时序。s6-svscan 在 0 秒就起来了,docker-proxy :2342 也立刻开始 LISTEN——但这只是"空房间有门牌号"。apt-get dist-upgrade 实际在跑 60–300 秒,加上 apt install 8 个包 还会再花 60–180 秒。Plus 标签的 250426 镜像在网络好的情况下也要 5 分钟起,差的时候 10–15 分钟。


为什么要保留 PHOTOPRISM_INIT 这个开关?

理解了行为之后,再回过头看这个开关本身:它的存在是有意义的。

PhotoPrism 官方设计 PHOTOPRISM_INIT 的时候,假定的部署场景是 “容器就是一台完整的小机器”——你装好容器之后第一次启动,让它去拉 TensorFlow 模型、装 Intel 驱动、装 Caddy HTTPS、装 ytdlp 等等。这种场景下:

但我的部署场景是反向的

devices:
  - /dev/dri/renderD128:/dev/dri/renderD128    # ← 关键:我把宿主机的核显直通进容器了

这意味着:

正确思路:让驱动存在于"能装它"那一层。我已经在 host 装好了驱动,那么容器只需要通过 device 文件"用"驱动就行。


解决方案:三个层级,三种修法

按"对配置的侵入程度"从小到大排列:

✅ 方案一(强烈推荐):把 PHOTOPRISM_INIT 删掉或清空

改 stack 配置

services:
  photoprism:
    image: 192.168.103.8:8082/photoprism/photoprism:250426
    container_name: photoprism
    # ... 其他不动 ...
    environment:
      # PHOTOPRISM_INIT: "intel"     ← 删掉这一行,或者改成空
      PHOTOPRISM_FFMPEG_ENCODER: "intel"
      # ... 其它环境变量照旧 ...
    devices:
      - /dev/dri/renderD128:/dev/dri/renderD128
    restart: always

在 Portainer 里的操作是:

  1. 进入 stack 的 Editor 视图
  2. PHOTOPRISM_INIT 那行直接删掉(或者改成 ""
  3. Update the stack不要勾选 “Re-pull image and redeploy”(我们只想重启应用层,不需要重新拉镜像)
  4. 30 秒内 2342 就会返回 307

为什么这招最干净?

我自己在 DSM 上 30 秒内验证通过:

更新前:apt 还在装,curl 拿 RST
更新后:直接 307 → /library/login

🟡 方案二:第一次"预烤"出 init 完的镜像

如果你出于某种执念(或者别的应用确实需要 PHOTOPRISM_INIT=intel),可以用一次性命令把 init 阶段预跑完,然后 commit 成新镜像:

# 1. 起一个一次性容器,让它把 init 跑完
docker run -d --name pp-init-tmp \
  -e PHOTOPRISM_INIT=intel \
  192.168.103.8:8082/photoprism/photoprism:250426

# 2. 等 10 分钟,docker logs 看 init 跑完没有
docker logs -f pp-init-tmp
# 看到 "photoprism: started" 之类就表示 init 完了

# 3. commit 一个 baked 镜像
docker commit pp-init-tmp 192.168.103.8:8082/photoprism/pp-baked:latest

# 4. 删掉临时容器
docker rm -f pp-init-tmp

之后 stack 改用 baked 镜像,不再设 PHOTOPRISM_INIT,启动速度也是 30 秒级。

代价是镜像大约会大 200–300MB(intel 包 + dist-upgrade 后的系统包)。但长期来看比每次启动都装一次更划算。

🟢 方案三:跳过 dist-upgrade?抱歉,没有这条路

理论上你可能想"我只要装 intel 包,不要 dist-upgrade”。这条路走不通——Makefile 里 intel: update install-intelupdate 永远会先跑 apt-get dist-upgrade。除非你 fork 镜像重写 Makefile,否则没有干净的"只装 intel"路径。

我列出来只是让你知道这个思路已经死路,不用再绕。


三种修法的对比

图 5:方案 1 是 99% 的人应该用的;方案 2 适合"我们真的要在容器里用 Intel 编码库"的场景;方案 3 是个走不通的对照。


怎么验证修复真的成功了?

不要用"等了多久"来判断。真正能确认服务可用的是这几条命令:

# 1. 容器内的进程树
docker exec photoprism ps -ef | grep -E 'photoprism|entrypoint|apt|make'
# 看到 /opt/photoprism/bin/photoprism start 才是真正起来了
# 看到 /scripts/entrypoint-init.sh 还活着 = 还卡着

# 2. 容器内监听
docker exec photoprism ss -ltn | grep 2342
# 或者
docker exec photoprism bash -c '</dev/tcp/127.0.0.1/2342 && echo OPEN'
# OPEN = 起来;空 = 还卡

# 3. 外部 curl
curl -I http://<NAS>:2342/
# 期望: HTTP/1.1 307 Temporary Redirect
#        Location: /library/login
# 期望不该出现的: Connection reset by peer

只有当 1+2+3 三条同时是"对",才说明服务真的可用。


这张是官方 PhotoPrism 的真实截图,作为修复成功的视觉对照

图 6:PhotoPrism 真实跑起来后的样子(官方截图)。能进这个界面,说明 2342 已经 307 → 登录页 → 库视图。


顺带说一句:其他常见"Up 但不可用"的情形

这次踩到的坑本质上是 “s6-overlay init 阶段没跑完,主进程没起”。这个模式在很多容器里都会复现,特征都差不多:

容器 第一次启动可能跑的 init 卡多久 怎么判断
PhotoPrism apt dist-upgrade + GPU 包 / TF 模型 5–15 min docker exec ... ps -ef | grep apt
Nextcloud 装 cron、初始化 SQLite 30s–2min docker logs
GitLab 拉 / reconfigure 整个 Omnibus 5–10 min gitlab-ctl reconfigure 日志
Home Assistant pip install 大量集成 1–5 min docker exec ... ps -ef | grep pip
Vaultwarden 一般 5s < 30s 看 80/3012 端口

统一判断法

docker exec <ctr> ps -ef | grep -E 'apt|pip|curl|make|init'
# 如果有任何一条还在跑 = init 没完

Q&A

Q1:我看到 PHOTOPRISM_INIT=intel 不是"装 Intel 加速"的意思吗?为什么不能保留它?

PHOTOPRISM_INIT=intel 确实是"装 Intel 加速",但它的实现方式是"在容器内重装一遍驱动+用户态库"。如果你已经 devices 直通了 /dev/dri/renderD128,那驱动和用户态库在宿主机上就有,容器内不需要再装——Linux device pass-through 的本意就是"用现成的,不重新装"。

而且 intel 目标强制会先跑 apt-get update && apt-get dist-upgrade——这不仅装驱动,还顺手给你升级整个 OS 包。这才是它卡 5–10 分钟的真正原因。

Q2:那 PHOTOPRISM_FFMPEG_ENCODER: "intel" 这一行还要不要?

保留。这一行是运行时配置,告诉 PhotoPrism “用 Intel QSV 做 ffmpeg 编码”,跟 PHOTOPRISM_INIT 是两码事。前者是"装东西",后者是"用东西"。

Q3:我用的是普通 photoprism/photoprism:latest,没设 PHOTOPRISM_INIT,为什么第一次启动也慢?

因为 PhotoPrism 的默认 inithttps tensorflow,会去拉 TensorFlow 模型(几百 MB)+ 装 caddy。这一套大概 2–5 分钟。如果你不想拉模型,把 PHOTOPRISM_DISABLE_TENSORFLOW: "true" 加上(连带人脸识别、图像分类都会关掉)。

Q4:docker ps 显示 Up 5 minutes 是不是就代表服务可用?

不代表Up 的语义是"容器内的 PID 1 进程没死"。对 PhotoPrism 这种用 s6-overlay 的镜像,PID 1 是 s6-svscan——它会一直活着,哪怕主进程没起来。唯一可信的信号是"容器内部 ss -ltn 看到端口被监听"。

Q5:Portainer 里看 stack 是绿的,是不是就代表正常?

不是。Portainer 的 health indicator 主要看两件事:容器是否 Running,以及(如果你配置了)Docker 自己的 healthcheck。PhotoPrism 镜像没有内置 healthcheck(你 stack 文件里也没加 healthcheck:),所以 Portainer 看到的"绿"和"Up"是同一件事——s6 监督树没死。这是另一个常见误解

Q6:能直接 docker exec ... kill 把 apt 杀掉吗?

不推荐。s6-overlay 的 stage 2 oneshot 是一次性跑完才放行后面的 service。你手动 kill 掉 aptentrypoint-init.sh 之后,s6 可能会:

如果你真的不想等、又不想改 stack,唯一相对安全的办法是:

docker compose -f /path/to/your/stack.yml pull
# 改完再
docker compose up -d

但这比改 stack 那一行还麻烦。所以——改 stack 那一行是性价比最高的方案

Q7:为什么官方 docker-compose 模板里还要写 PHOTOPRISM_INIT: "intel"

PhotoPrism 官方 compose 模板面向的是最广泛的场景——用户在裸 Docker Desktop、Synology、unRAID 上跑,不一定有 device 直通。模板写上这一行是"我替你把驱动装好"的兜底,对没有直通的部署反而是贴心的。

已经 device 直通的高级用户,这就是冗余甚至反作用。没有银弹,理解自己部署的每一层在做什么,比照抄模板重要


写在最后

这次排查的体会:

下次再看到"Portainer 是绿的,但浏览器连不上"——别急着重启容器。先 docker exec ... ps -ef 看一下主进程在哪


参考链接


本文涉及的所有配置、命令、IP、域名均已在公开前做脱敏处理;具体网络拓扑以你自家环境为准。