我以为只是 pull 一下,结果整个架构被重写了

自建 Docker 镜像加速服务这件事,我跑了一年多。服务器放海外,专治一个老毛病 ——docker pull 官方镜像动不动就卡死、超时、EOF。

前阵子我想着,这服务也该例行更新一下了。心里想的就是一行命令的事:pull 一下、up 一下,完事。

结果一翻上游仓库,我愣住了。这哪是更新,这是一场迁移。

本来只想 pull,结果发现不对劲

先看版本。上游 dqzboy/Docker-Proxy 最新 release 是 v5.1.4,8 月 13 号发的。变更列表不长:加了个一键改密码脚本、修了登录页 session 状态、给 WebSocket 加了鉴权。

看着挺温和,对吧。但翻 commit 历史才发现,7 月 21 号上游干了一件大事 —— 把后端整个重写了。

旧架构是 distribution registry,镜像拉下来会落盘缓存。新架构直接换成了纯 Go 实现,零磁盘缓存,按 Host 自动路由到 Docker Hub、GHCR、Quay、K8s 这些仓库。服务端做完 token 鉴权就流式转发,不落盘、不占本地存储。

架构一换,compose 文件跟着大改。我把服务器上的旧 compose 和上游新 compose 摆在一起:

项目旧架构新架构
服务数3 个2 个
后端dockerhub(distribution registry,磁盘缓存)go-proxy(Go 零缓存)
管理面板registry-ui已删除,功能并入 hubcmd-ui
后端端口5000150000
UI 端口5000330080
后端配置registry-hub.ymlconfig.yaml(自动播种)
UI 数据目录hubcmd-datadata/hubcmd-ui

看到第三行我就警觉了:registry-ui 这个组件被上游直接删了。

更麻烦的是下面这个坑 —— 我服务器上的 hubcmd-ui:latest 早就是新版了,但这个新版要一个环境变量 GO_PROXY_ADMIN_TOKEN 才能连后端,而我的旧 compose 里根本没这个变量。

换句话说,如果我真按原计划直接 docker compose pull && up -d,面板会拉起来,但连不上后端,白屏。

这就是 :latest 的代价 —— 标签没变,镜像已经换代了。

迁移过程

既然躲不掉,那就正经迁。思路清晰,四步走。

第一步,备份。compose、.env、config、数据目录全拷一份。唯独那个 2.3G 的 registry 缓存目录不用动 —— 新架构零缓存,它本来就不参与。

Terminal window
cd /path/to/docker-proxy
cp -a docker-compose.yaml .env config hubcmd-data docker-proxy.bak.$(date +%F)/

第二步,重写 compose。这里我做了个关键决定:端口保持不变。go-proxy 继续用 50001,hubcmd-ui 继续用 50003,这样外面配的反代、隧道统统不用动。

services:
go-proxy:
image: ${REGISTRY_IMAGE:-dqzboy/registry:latest}
container_name: reg-go-proxy
restart: always
environment:
- OTEL_TRACES_EXPORTER=none
- GO_PROXY_ADMIN_TOKEN=${GO_PROXY_ADMIN_TOKEN:-change-me-to-a-strong-random-token}
volumes:
- ./config/go-proxy:/app/config.d
ports:
- 50001:5000
networks:
- registry-net
21 collapsed lines
hubcmd-ui:
image: ${UI_IMAGE:-dqzboy/hubcmd-ui:latest}
container_name: hubcmd-ui
restart: always
depends_on:
- go-proxy
environment:
- GO_PROXY_ADMIN_URL=http://go-proxy:5001
- GO_PROXY_ADMIN_TOKEN=${GO_PROXY_ADMIN_TOKEN:-change-me-to-a-strong-random-token}
- HOST_NAME=${HOST_NAME:-}
- SECURE_COOKIE=false
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data/hubcmd-ui:/app/data
ports:
- 50003:3000
networks:
- registry-net
networks:
registry-net:

第三步,生成 .env。新架构要两样东西:一个随机 admin token、主机名。

Terminal window
cat > .env <<EOF
GO_PROXY_ADMIN_TOKEN=$(openssl rand -hex 24)
HOST_NAME=$(hostname)
REGISTRY_IMAGE=dqzboy/registry:latest
UI_IMAGE=dqzboy/hubcmd-ui:latest
EOF

第四步,迁移数据目录,然后启动。

Terminal window
mkdir -p data
cp -a hubcmd-data data/hubcmd-ui # 旧数据 copy 过去,原目录留着当后路
docker compose config --quiet && echo OK
docker compose pull
docker compose up -d

起来之后,三个验证全过才算成功。

Terminal window
# 1. 拉镜像端点活着
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:50001/v2/
# 2. 管理面板活着
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:50003/
# 3. 真拉一个镜像,确认走的是新代理
docker pull 127.0.0.1:50001/library/busybox:latest

busybox 拉下来的那一刻,心里踏实了。真的走的是新代理,不是本地缓存。

踩坑记录

记几个这次撞上的坑,下次能少走弯路。

:latest 是个温柔的陷阱。 你以为 pull 一下就是最新,其实它可能已经跨过了一次架构换代,配置早就对不上了。真要稳,pin 个 tag 比什么都强。

别死守被上游删掉的组件。 registry-ui 这个组件没了就是没了,别想着「我再单独拉一个跑起来」—— 那是在跟上游对着干。

旧磁盘缓存别心疼。 2.3G 的 registry 缓存在零缓存架构下直接作废,删了就删了。反正源头在 Docker Hub,随时能再拉。

迁移之后,拉取又失败了

迁移完,我把域名配上去,指到 go-proxy。

先用浏览器打开,404。第一反应是「坏了,没配好」。

其实不是。go-proxy 是拉镜像的 API,不是网页,根路径本来就什么都没有。浏览器 404 是正常现象 —— 真正该测的是 /v2/ 端点和 docker pull。

结果一测,pull 还真拉不动,manifest 接口返回 502。

查日志,go-proxy 报的是:

token endpoint returned 401: {"details":"incorrect username or password"}

原来我在面板里给 dockerhub 段填了 Docker Hub 的账号密码,本想着提一下匿名拉取的频率限制。但 Docker Hub 现在的 token 端点只认 Personal Access Token(PAT),密码直接填进去就是 401。go-proxy 拿不到 token,就把 502 抛给了上游。

解决办法简单粗暴:把 username / password 清空,走匿名。公开镜像的拉取,匿名完全够用。

清空、重启,再拉一遍 ——alpine 下来了,域名也通了。

「网页 404」和「拉不了镜像」,一个是正常现象,一个是真 bug。前者别慌,后者查日志。

总结

这次更新教会我一件事:自托管服务里的 :latest,等于把「要不要迁移」的决定权,交到了上游手里。

下次再看到 compose 变了,别急着 pull。先看看 commit 里,是不是藏着一场重写。