PhotoPrism 启动 5 分钟还在装系统包?一次 PHOTOPRISM_INIT=intel 的踩坑与正确配置
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 正常运行时的样子——如果你看到的是这个画面,说明你一切正常;如果看到的是 Connection reset by peer,请继续往下读。
问题背景:一台 NAS 跑 PhotoPrism 的"标准姿势"
我家里有一台基于 Synology DSM 的 NAS,平时跑几十个 Docker 容器:Portainer、Syncthing、qbittorrent、Jellyfin、Photoprism、photoprism-ro …… 全都是用 Portainer 的 stack 部署。
PhotoPrism 是两个实例同时跑:
- 主实例
photoprism,管理员账号、可读可写 - 只读实例
photoprism-ro,只读库,作为只读视图供其他人访问
两者的 docker-compose.yml 都长得几乎一样,最关键的"差异化"那行长这样:
environment:
PHOTOPRISM_INIT: "intel"
加上这一行是为了让 PhotoPrism 在容器里用上 Intel 核显做 QSV 硬解——毕竟 PhotoPrism 跑视频转码的时候,软件转码的 CPU 占用会爆。
一切看上去都挺合理的,对吧。
问题表现:容器是 Up 的,但访问就是连不上
部署完 stack 之后,我去 Portainer 看了一眼——
Status: Up 3 minutes(healthy 状态)- 端口映射正常:
0.0.0.0:2342->2342/tcp
在主机上 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.sh → make -C /scripts intel → apt-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 进程在监听"才代表服务真的可用。
图 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 官方 Makefile 的 intel 目标实际展开为:
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。
图 2:同一个时刻,主机上、docker-proxy、容器内 三层"Up"的语义对比。Portainer 的绿点只反映最外层——s6-svscan 进程没死,仅此而已。要判断服务是否真的可用,必须进到容器里看 ss -ltn 和 ps -ef。
为什么 curl 拿到的是 RST?
在搞清楚根因之后,还要解释一个看似更奇怪的细节:为什么 ss -ltn 在主机上明明显示 :2342 在 LISTEN,curl 过去却被对端 RST 回来?
这是 Docker 的 docker-proxy(也叫做 dockerd-proxy)的标准行为。
当你 docker run -p 2342:2342 的时候,docker 会在主机上启动一个 docker-proxy 进程,它的工作就是:
- 在主机上监听
0.0.0.0:2342(这就是你ss -ltn看到的那行 LISTEN) - 接受 TCP 连接
- 把连接转发到容器的 network namespace里
127.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 起不来",而是"你以为它没起"。
图 3:左图:浏览器 / curl 从主机外部访问 :2342,拿到 RST;右图:docker exec photoprism ps -ef 看到真相——主进程被 apt-get dist-upgrade 卡住,连 listen 都没起来。
图 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 等等。这种场景下:
- 容器内没有宿主机的设备直通
- 容器内没有预装的 GPU 用户态工具
- “装系统级包"反而是合理的
但我的部署场景是反向的:
devices:
- /dev/dri/renderD128:/dev/dri/renderD128 # ← 关键:我把宿主机的核显直通进容器了
这意味着:
- 容器内不需要装
intel-opencl-icd、libmfx-gen1.2、va-driver-all,这些在宿主机上已经存在并通过 device 暴露 - 容器内只需要让 Go 代码通过
/dev/dri/renderD128调宿主机的 QSV 硬解 - 整段
apt dist-upgrade+ 装 8 个 GPU 包 = 纯浪费时间
正确思路:让驱动存在于"能装它"那一层。我已经在 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 里的操作是:
- 进入 stack 的 Editor 视图
- 把
PHOTOPRISM_INIT那行直接删掉(或者改成"") - 点 Update the stack,不要勾选 “Re-pull image and redeploy”(我们只想重启应用层,不需要重新拉镜像)
- 30 秒内
2342就会返回 307
为什么这招最干净?
- 你的驱动在 host,device 直通到容器,用驱动这件事不需要在容器里装包——这是 Linux device pass-through 的本意
PHOTOPRISM_FFMPEG_ENCODER: "intel"保留不动——这一行才真正决定 PhotoPrism 用哪种编码器后端,它和PHOTOPRISM_INIT是两件不同的事- 你的
volumes、MYSQL外部数据库、/dev/dri/renderD128全部不动,原来的数据、账号、索引全部保留
我自己在 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-intel,update 永远会先跑 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 三条同时是"对",才说明服务真的可用。

图 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 的默认 init 是 https 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 掉 apt 或 entrypoint-init.sh 之后,s6 可能会:
- 把整个 oneshot 标失败 → photoprism service 永远不启动
- 直接放行(不重跑)→ 后面缺包,photoprism 启动失败
- 行为不可预测
如果你真的不想等、又不想改 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 直通的高级用户,这就是冗余甚至反作用。没有银弹,理解自己部署的每一层在做什么,比照抄模板重要。
写在最后
这次排查的体会:
- “Up"是个很弱的状态词。它只保证 PID 1 活着,不保证服务活着。永远用
ss -ltn在容器内确认端口监听。 docker-proxy在 host 上 LISTEN ≠ 应用可用。host 看到 LISTEN 是因为 docker-proxy 提前占位,连接到了它才去容器里做 connect,容器里没人 listen 就会 RST。- 官方模板不是圣经。
PHOTOPRISM_INIT=intel在官方语境里是"贴心兜底”,在你已经 device 直通的语境里是"装系统"——同一个配置在不同部署下意义可以完全相反。
下次再看到"Portainer 是绿的,但浏览器连不上"——别急着重启容器。先 docker exec ... ps -ef 看一下主进程在哪。
参考链接:
- PhotoPrism 官方文档 - 环境变量
- PhotoPrism 官方文档 - Docker Compose 部署
- PhotoPrism GitHub 仓库
- s6-overlay 项目说明
- Docker userland-proxy 实现细节
- Docker 网络模型 - 已发布端口
本文涉及的所有配置、命令、IP、域名均已在公开前做脱敏处理;具体网络拓扑以你自家环境为准。