Warning
本仓库是基于 project-zot/zot 的二次开发,
是非 Zot 官方发行版。当前发行版固定上游 Zot v2.1.20。本项目不能保证跨 Zot 大版本
时配置格式永远兼容;升级前必须备份并完成独立环境验证。
这个项目把 Zot 配置成 Docker Hub 的按需缓存镜像,并增加一个独立、轻量的管理服务。 管理员可以在 Web 页面中选择:
- 公网匿名镜像加速:默认模式。Docker 客户端可把站点设为
registry-mirrors,继续用docker pull hello-world:linux这类普通镜像名。 - 私有镜像加速:匿名访问返回 401。客户端登录后,使用完整站点域名显式拉取,例如
docker pull registry.example.com/library/hello-world:linux。
生产镜像:ghcr.io/rainlvan/zot:2.1.20-r1
2.1.20-r1 是推荐的固定标签;2.1.20 和 latest 是方便标签,latest 不建议用于生产环境。
本项目直接使用上游 Zot v2.1.20 的完整可执行文件,保留 Zot 的 OCI Registry、Web UI、
搜索和 Sync 扩展能力。新增部分主要是:
- 同一个镜像中的严格入口点、幂等初始化器和健康检查;
- 独立运行的
mirror-admin管理服务; - 公网/私有模式的事务式切换控制器;
- Docker Compose、1Panel、Nginx、Caddy 和 TLS/HTTPS 示例;
- GitHub Actions 双架构构建与 GHCR 发布;
- 面向部署者的安全、兼容性、备份和回滚说明。
上游项目、许可证和 NOTICE 归属见 NOTICE。本仓库不是 project-zot 团队维护的 分支,也不代表上游对这里的管理服务、镜像或部署方式提供支持。
管理页没有引入数据库、Node.js 运行时或额外应用框架。它使用 Python 标准库提供静态页面 和很小的 JSON API,只保存当前结果、私有访问策略和配置备份。Zot 与管理服务是两个独立 容器,但来自同一个固定镜像,因此:
- Zot 和管理服务可以分别查看日志、健康状态与重启;
- 管理服务故障不会替代 Zot 的数据目录;
- 发布物只有一个镜像,版本与回滚关系清楚;
- 配置、凭据、状态和 registry 数据全部保存在宿主机的
ZOT_ROOT; - 管理 API 只接受反向代理注入的 64 位十六进制密钥,浏览器拿不到该密钥。
管理服务需要挂载 Docker socket 来校验候选配置并重启 Zot。即使 Compose 中写成只读挂载,
Docker socket 仍应视为接近宿主机 root 的权限。不要运行未经审查的管理镜像,不要把
5001 直接开放到公网。
每次切换都执行以下流程:
- 读取现有配置并只修改模式相关字段;
- 写入同目录的候选配置;
- 使用目标 Zot 镜像执行
zot verify验证候选配置; - 备份当前配置并原子替换;
- 重启 Zot,等待容器健康;
- 探测
/v2/,公网模式必须为 200,私有模式必须为 401; - 任一步失败则回滚旧配置并再次确认健康。
这种“先验证候选配置,再切换,失败则回滚”的方式能降低配置错误导致长时间中断的风险, 但不能保证跨大版本兼容,也不能替代数据备份和升级演练。详细边界见 兼容性说明。
你需要:
- 一台安装 Docker Engine 与 Docker Compose v2 的 Linux 服务器;
- 一个解析到服务器公网 IP 的域名,例如
registry.example.com; - 公网只开放 TCP 80/443;
- 能从服务器访问 Docker Hub、GHCR 和证书 ACME 服务;
- 足够的磁盘空间保存缓存层和你的私有镜像。
不要在安全组或防火墙中开放 5000/5001。Compose 只把它们绑定为
127.0.0.1:5000 和 127.0.0.1:5001,TLS 由同机反向代理终止。
以下命令假定:
- 部署根目录是
/opt/zot; - 示例域名是
registry.example.com; - 你已把本仓库克隆到
/opt/zot-src。
请把示例域名替换成自己的域名。
sudo install -d -m 700 /opt/zot
sudo install -m 644 /opt/zot-src/compose.yaml /opt/zot/compose.yaml
sudo install -m 600 /opt/zot-src/.env.example /opt/zot/.env
DOMAIN=your-registry.example.net
sudo sed -i "s|registry.example.com|$DOMAIN|g" /opt/zot/.env上面的 sed 左侧是仓库示例值,右侧应改成你的真实域名。确认固定镜像与目录:
sudo grep -E '^(ZOT_IMAGE|ZOT_ROOT|ZOT_PUBLIC_ORIGIN)=' /opt/zot/.env预期包含:
ZOT_IMAGE=ghcr.io/rainlvan/zot:2.1.20-r1
ZOT_ROOT=/opt/zot
ZOT_PUBLIC_ORIGIN=https://registry.example.com显式运行初始化可以确保 Zot 容器创建前,配置和 htpasswd 文件已经存在:
cd /opt/zot
sudo docker compose pull
sudo docker compose run --rm init
sudo docker compose up -d
sudo docker compose ps初始化器可重复运行。它会保留所有非空配置、凭据、管理状态和数据,不会把已有文件重置为 默认值。
没有提供 Docker secret 时,初始化器会生成一次性明文交接文件:
sudo cat /opt/zot/mirror-admin/state/initial-admin-password用户名是 zotadmin。把密码保存到密码管理器并验证登录后,删除明文交接文件:
sudo rm /opt/zot/mirror-admin/state/initial-admin-password删除它不会删除 htpasswd 中的 bcrypt 哈希。不要把初始密码、.env、htpasswd、代理密钥或
任何 TLS 私钥提交到 Git。
| 路径 | 内容 | 是否必须备份 |
|---|---|---|
/opt/zot/compose.yaml、.env |
部署版本与环境 | 是 |
/opt/zot/config/ |
Zot 配置与 htpasswd | 是 |
/opt/zot/data/ |
缓存镜像、私有镜像、元数据 | 是 |
/opt/zot/mirror-admin/state/ |
代理密钥、模式状态、配置备份 | 是 |
这一节中的证书申请必须通过 1Panel 左侧导航完成,不是在 1Panel 网页终端里运行 ACME 命令。官方入口是 网站 → 证书。
- 打开 1Panel 左侧 网站,进入网站列表。
- 点击创建网站,选择 反向代理。
- 主域名填写
registry.example.com。 - 代理地址填写
http://127.0.0.1:5000。 - 创建后先保持后端为 HTTP;Zot 的 5000 端口不对公网开放。
1Panel 生成的 / 反向代理负责 Registry API 和 Zot UI。大镜像上传/下载需要关闭请求与
响应缓冲,并适当提高超时;仓库提供的 Nginx/OpenResty 示例已经包含这些设置。
- 在左侧进入 网站 → 证书。
- 首次使用先创建 ACME 账户,填写有效邮箱并同意 CA 条款。
- DNS 托管商支持 API 时,先创建 DNS 账户,再选择 DNS 验证;单域名也可按 1Panel 页面 提示使用 HTTP 验证。
- 点击申请证书,主域名填写
registry.example.com,选择刚创建的 ACME/DNS 账户。 - 申请成功后确认状态正常、到期时间正确,并开启自动续签。
- 回到该网站的 HTTPS 设置,启用 HTTPS,选择刚申请的证书,并启用 HTTP 跳转 HTTPS。
若使用 HTTP 验证,公网 80 必须能到达 1Panel/OpenResty;DNS 必须已经指向服务器。证书 下载包可能含私钥,只能通过安全渠道保存。
先把同一个管理员 bcrypt 文件放到 1Panel 网站目录:
sudo install -D -m 600 \
/opt/zot/config/htpasswd \
/www/sites/zot/.mirror-admin-htpasswd再用仓库脚本把内部代理密钥写入只允许 root 读取的配置片段。脚本只输出目标路径,不输出 密钥内容:
cd /opt/zot-src
sudo ZOT_ROOT=/opt/zot ./scripts/render_proxy_config.sh \
deploy/1panel-locations.conf.template \
/www/sites/zot/mirror-admin-locations.conf在 1Panel 的网站配置页面编辑该站点的 OpenResty/Nginx 配置,在 HTTPS server { ... }
内部加入:
include /www/sites/zot/mirror-admin-locations.conf;保存前使用 1Panel 页面提供的配置检查;保存后让 1Panel 重载网站配置。不要把包含代理密钥 的渲染结果粘贴到 Issue、聊天或仓库。
最终访问:
https://registry.example.com/_mirror-admin/
浏览器先显示 Basic Auth,用户名为 zotadmin。通过后才能看到简单的公网/私有模式选项。
curl -i https://registry.example.com/v2/
curl -I https://registry.example.com/_mirror-admin/默认公网模式下 /v2/ 应返回 200;管理路径未提供 Basic Auth 时应返回 401。若返回 502,
先在 1Panel 宿主机确认 127.0.0.1:5000 和 127.0.0.1:5001 正在监听。
1Panel 官方文档:
仓库的 Nginx 示例包含:80 端口 ACME/HTTPS 跳转、TLS 1.2/1.3、 Registry 无限请求体、900 秒超时、关闭代理缓冲,以及受 Basic Auth 保护的管理路径。
- 把示例复制到 Nginx 配置目录并替换域名与证书路径。
- 使用渲染脚本替换
@@PROXY_SECRET@@:
cd /opt/zot-src
sudo ZOT_ROOT=/opt/zot ./scripts/render_proxy_config.sh \
deploy/nginx.conf.example \
/etc/nginx/conf.d/zot.conf
sudo nginx -t
sudo systemctl reload nginx- 确认
/etc/nginx/conf.d/zot.conf权限为 0600,且 Nginx master 能读取/opt/zot/config/htpasswd。 - 证书可由 certbot、发行版 ACME 工具或已有企业 CA 提供。不要在公网使用自签证书, 除非所有客户端都已正确安装私有 CA。
如果 Nginx 本身运行在容器中,127.0.0.1 指向 Nginx 容器,而不是宿主机;应使用受控的
Docker 网络或明确的宿主机网关地址,并继续保证 5000/5001 不直接暴露公网。
Caddy 示例使用域名触发自动 HTTPS,并给管理路径增加
basic_auth 与内部代理密钥。Caddy 需要公网 80/443 且 DNS 已生效。
- 复制并编辑域名:
sudo install -m 644 /opt/zot-src/deploy/Caddyfile.example /etc/caddy/Caddyfile
DOMAIN=your-registry.example.net
sudo sed -i "s/registry.example.com/$DOMAIN/g" /etc/caddy/Caddyfile- 交互式生成 Caddy 需要的密码哈希,不要在命令参数中写明文密码:
sudo caddy hash-password- 创建 root-only 的
/etc/caddy/zot.env,写入两行:
CADDY_ADMIN_HASH=上一步输出的哈希
ZOT_PROXY_SECRET=从/opt/zot/mirror-admin/state/proxy-secret读取的64位值- 权限与 systemd 配置:
sudo chmod 600 /etc/caddy/zot.env
sudo systemctl edit caddy在编辑器中加入:
[Service]
EnvironmentFile=/etc/caddy/zot.env然后验证并重载:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl daemon-reload
sudo systemctl restart caddyCaddy 使用真实域名时会自动申请和续签公网可信证书。官方说明见 Automatic HTTPS 和 reverse_proxy。
在 Linux Docker Engine 的 /etc/docker/daemon.json 中加入:
{
"registry-mirrors": ["https://registry.example.com"]
}若文件已有其他配置,请合并 JSON,不要覆盖。验证并重启 Docker:
sudo dockerd --validate --config-file=/etc/docker/daemon.json
sudo systemctl restart docker
docker info | sed -n '/Registry Mirrors/,+3p'
docker pull hello-world:linuxDocker Desktop 用户在 Settings → Docker Engine 合并同一段 JSON,然后 Apply & Restart。
这里的 registry-mirrors 是 Docker Hub pull-through mirror。它不会自动代理任意第三方
registry。Docker 官方说明见
Mirror the Docker Hub library。
在管理页切换到私有模式后,不再依赖普通镜像名的匿名 mirror 流程。先登录,再使用完整 域名显式拉取:
docker login registry.example.com
docker pull registry.example.com/library/hello-world:linux用户名默认是 zotadmin。使用 --password-stdin 可避免把密码写入命令历史:
printf '%s\n' "$ZOT_PASSWORD" | \
docker login registry.example.com --username zotadmin --password-stdin不要在共享 Shell 历史、CI 日志或 Compose 文件中保存明文密码。
打开 https://registry.example.com/_mirror-admin/。页面只提供当前模式、Zot 健康状态、最近
一次切换结果、数据量,以及两个简单选项。默认开启公网匿名镜像加速。
切换期间页面会暂时禁用按钮。成功返回前,控制器已经完成候选配置校验、备份、原子切换、 重启、健康检查与模式探测。若显示失败,当前请求会返回错误码,控制器会尝试恢复旧配置; 请查看日志确认回滚健康后再重试:
cd /opt/zot
sudo docker compose logs --tail=200 mirror-admin zot
sudo docker compose ps只备份 Compose 文件不够。完整备份必须包含镜像缓存、私有镜像、Zot 配置、htpasswd、管理
状态、配置备份、Compose 和 .env。
cd /opt/zot
sudo docker compose stop mirror-admin zot
sudo tar --acls --xattrs -C /opt \
-czf /safe-backups/zot-$(date +%Y%m%d-%H%M%S).tar.gz zot
sudo docker compose start zot mirror-admin
sudo sha256sum /safe-backups/zot-*.tar.gz备份期间 registry 暂停服务,但文件系统状态一致。在线文件复制可能跨越写入边界,只适合 你明确接受该一致性风险,或底层存储提供一致快照时使用。
- 在新服务器安装相同或已验证兼容的 Docker/Compose。
- 校验备份 SHA-256。
- 确认目标
/opt/zot不包含需要保留的数据。 - 停止旧服务,把现有目录移动到带时间戳的保留目录,再解压备份。
- 先用固定镜像运行初始化/校验,再启动:
cd /opt/zot
sudo docker compose run --rm init
sudo docker compose up -d
sudo docker compose ps恢复后分别验证公网或私有流程。不要在没有验证备份的情况下删除旧目录。
- 创建包含数据的完整备份。
- 把
.env中ZOT_IMAGE改成明确的新修订标签,不要直接改为latest。 - 在旧容器仍运行时拉取镜像并验证现有配置:
cd /opt/zot
sudo docker compose pull
sudo docker compose run --rm init- 验证成功后执行
sudo docker compose up -d,再做公网和私有端到端测试。
把 .env 的 ZOT_IMAGE 改回上一个固定标签,然后:
cd /opt/zot
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps配置切换失败的自动回滚只恢复配置文件,不会自动回滚镜像版本或 registry 数据格式。跨 Zot 大版本回滚必须参考上游迁移说明和你自己的恢复演练。
cd /opt/zot
sudo docker compose down这会删除项目容器和网络,但保留 /opt/zot 下的绑定目录。删除 /opt/zot 会永久删除缓存、
私有镜像、配置和凭据;只有在已验证备份且确认不再需要数据时才手工执行。
sudo ss -lntp | grep -E '127\.0\.0\.1:(5000|5001)'
cd /opt/zot && sudo docker compose ps
cd /opt/zot && sudo docker compose logs --tail=200 zot mirror-admin确认反向代理和 Docker 在同一宿主机;若代理在容器中,不要把容器内的 127.0.0.1 当作
宿主机。
- 确认 Basic Auth 用户名是
zotadmin; - 确认反向代理读取的是当前
/opt/zot/config/htpasswd副本; - 修改 Zot 密码后,同步更新 1Panel 的
.mirror-admin-htpasswd; - 不要删除代理注入的
X-Mirror-Proxy-Secret。
- 检查 Docker 客户端的
registry-mirrors和 Docker daemon 重启结果; - 检查服务器能否访问 Docker Hub;
- 查看 Zot 日志中的 Sync 错误、上游限流和磁盘空间;
- 确认配置保留
onDemand: true、preserveDigest: true和http.compat: ["docker2s2"]。
Zot 官方配置说明明确指出:使用 preserveDigest: true 时必须同时启用 docker2s2,否则
Zot 会拒绝启动。参考 Configuring Zot。
确认 Nginx/OpenResty 有 client_max_body_size 0,并关闭
proxy_request_buffering/proxy_buffering。同时检查 CDN、WAF 或上游代理是否另有限制。
- DNS 必须解析到当前服务器;
- HTTP 验证需要公网 80;
- DNS 验证需要正确的 API 权限;
- 在 1Panel 网站 → 证书 查看失败详情,而不是在网页终端重新创建一套独立 ACME 状态。
- 安全报告与部署边界见 SECURITY.md。
- 支持矩阵和大版本升级要求见 docs/compatibility.md。
- 公网匿名服务应监控带宽、磁盘、上游限流和滥用;必要时加 CDN/WAF、限速或访问策略。
- Docker socket 是高权限边界;管理端口不得公网直连。
- TLS/HTTPS 只保护传输,不替代管理认证、强密码、主机加固和备份。
python -m unittest discover -s tests -v
python -m compileall -q mirror_admin container tests
bash -n container/*.sh scripts/*.sh ops/*.sh
docker build -t zot-project:dev .Linux 端到端测试会创建唯一 /tmp 目录、唯一容器/网络/镜像,并在退出时做有界清理:
bash scripts/smoke_container.sh成功时必须看到:
init_idempotent=ok
zot=healthy
mirror_admin=healthy
public_ordinary_pull=ok
private_anonymous=401
private_authenticated_explicit_pull=ok
final_mode=public
cleanup=ok
本项目使用 Apache License 2.0。感谢 project-zot/zot 提供生产级 OCI Registry 基础能力。 Zot 的安装与配置资料可参考 Zot v2.1.20 文档。