Zot 2.1.22 Docker Compose 部署指南

在 Docker 服务器上用 Compose 部署 Zot 2.1.22:镜像选型、htpasswd 账号、推拉闭环、数据持久化、等价 docker run 与运维 FAQ,实测通过。

ZotZot2026/11/01

Zot 2.1.22 Docker Compose 部署指南

目标:在装有 Docker 的服务器上以 Docker Compose 部署 OCI 原生镜像仓库 Zot v2.1.22(project-zot/zot),覆盖镜像选型、编排文件与配置文件编写、htpasswd 账号生成、启动、API 与推拉闭环验证、数据持久化、等价 docker run 命令与日常运维。已在 Ubuntu 24.04.5 LTS(x86_64)+ Docker 29.9.2 + Docker Compose v5.6.1 环境实测通过(2026-11-01)。Zot 是 OCI 镜像仓库,与文献管理软件 Zotero 无关;选型背景见配套《Zot 介绍与选型指南》。

一、环境信息

本节给出本次部署的宿主环境与版本基准,文中全部命令与回显均产生于该环境。

项取值
操作系统Ubuntu 24.04.5 LTS(x86_64)
Docker29.9.2(build a82e528)
Docker Composev5.6.1(docker compose 插件语法)
应用镜像ghcr.io/project-zot/zot:v2.1.22(full 构建)
端口规划宿主机 8150 → 容器 5000(HTTP,未启用 TLS)
编排目录/opt/data/quzuo/tmp/zot/stack(compose 与配置文件)
数据目录/opt/data/quzuo/tmp/zot/data(挂载为容器 /var/lib/registry)
账号admin / reader,口令均为 q1w2e3r4(bcrypt 散列存储)

版本自检实测回显:

$ docker --version
Docker version 29.9.2, build a82e528

$ docker compose version
Docker Compose version v5.6.1

说明:文中部署目录为实验环境实际路径;生产环境可换成 /opt/zot 之类的规划路径,同步修改编排文件中的挂载路径即可。

二、镜像选型说明

zot 官方在 GitHub Container Registry 发布多个镜像,本文采用 full 版:

镜像内容适用场景
ghcr.io/project-zot/zotfull 构建,含 search(CVE)、ui、sync、scrub、trust、metrics、lint、mgmt、userprefs 等全部扩展需要 Web UI、搜索、同步、验签的私有仓库(本文)
ghcr.io/project-zot/zot-minimal仅 dist-spec 核心代码与依赖只要推拉镜像、追求最小攻击面
ghcr.io/project-zot/zxpPrometheus exporter给 minimal 部署补指标
ghcr.io/project-zot/zb压测工具容量规划

镜像拉取实测回显(本机已有镜像,显示 up to date):

$ docker pull ghcr.io/project-zot/zot:v2.1.22
v2.1.22: Pulling from project-zot/zot
Digest: sha256:96cda11459ce6f8c60da3b03f03e9f67fd11b3ae2d82c4c04c92b3c608996080
Status: Image is up to date for ghcr.io/project-zot/zot:v2.1.22
ghcr.io/project-zot/zot:v2.1.22

full 镜像内置一份默认配置 /etc/zot/config.json(实测从镜像文件系统导出)。不挂载自定义配置时,zot 就按它运行:

{
  "storage":{
      "rootDirectory":"/var/lib/registry"
  },
  "http":{
      "address":"0.0.0.0",
      "port":"5000",
      "compat": ["docker2s2"]
  },
  "log":{
      "level":"debug"
  },
  "extensions": {
    "search": {
        "enable": true,
        "cve": {
            "updateInterval": "2h"
        }
    },
    "ui": {
        "enable": true
    },
    "mgmt": {
      "enable": true
    }
  }
}

要点:默认监听 0.0.0.0:5000,数据落在 /var/lib/registry,启用 search(含 CVE 扫描)、ui、mgmt 三个扩展,没有 auth 段(匿名可读可写),日志 debug 级输出到 stderr。生产使用必须像本文第四章那样挂载自己的配置补上认证。

💡 技术拓展:「镜像仓库」一词在 Docker 生态里有多个实现,容易与官方 registry 镜像混淆,三者关系如下——

对象镜像定位
zotghcr.io/project-zot/zotOCI 原生仓库,单二进制内置 UI/搜索/同步/验签
distribution(Docker Registry)registry:2协议最小实现,无 UI、无扩展
Harborghcr.io/harbor/harbor-*企业级平台,多组件(项目空间/RBAC/复制)

三者服务端都遵循 OCI Distribution Spec,docker、podman、skopeo 等客户端用法完全一致;本文选 zot,是为了用单容器同时获得协议服务 + Web UI + CVE 搜索。

三、前置条件

  1. 服务器已安装 Docker 与 Docker Compose v2 及以上(docker compose version 有回显,本文实测 v5.6.1);
  2. 可访问 ghcr.io 拉取镜像;网络不通时先为 Docker 守护进程配置代理或镜像加速;
  3. 宿主机 8150 端口空闲(ss -ltn | grep 8150 应无输出;被占用则换端口并同步修改编排文件,见第十一章 FAQ);
  4. 创建编排与数据目录:
mkdir -p /opt/data/quzuo/tmp/zot/stack /opt/data/quzuo/tmp/zot/data

四、部署步骤

4.1 准备编排文件、配置与账号文件

在 /opt/data/quzuo/tmp/zot/stack/ 下创建三个文件。

docker-compose.yml(全文,与盘上实际文件一致):

name: zot-2.1.22-stack
services:
  zot:
    image: ghcr.io/project-zot/zot:v2.1.22
    container_name: zot-2-1-22-registry
    hostname: zot-registry
    restart: unless-stopped
    ports:
      - "8150:5000"
    volumes:
      - ./config.json:/etc/zot/config.json:ro
      - ./htpasswd:/etc/zot/htpasswd:ro
      - /opt/data/quzuo/tmp/zot/data:/var/lib/registry
    networks:
      zot-net:
        ipv4_address: 10.89.60.10
networks:
  zot-net:
    name: zot-2.1.22-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.60.0/24

说明:

  • zot 镜像入口命令固定为 serve /etc/zot/config.json(docker inspect 实测:/usr/local/bin/zot-linux-amd64 [serve /etc/zot/config.json]),所以配置与口令文件必须挂到容器内这两个路径;
  • 两个配置文件以 :ro 只读挂载,防止进程误写;
  • 数据挂载点 /var/lib/registry 与镜像默认 storage.rootDirectory 一致;
  • 网络显式指定 10.89.60.0/24 网段,避免与其他 Compose 项目默认网段冲突。

config.json(全文,相对镜像默认配置补上认证、把日志级别收窄并落盘):

{
  "distSpecVersion": "1.1.0",
  "storage": {
    "rootDirectory": "/var/lib/registry",
    "dedupe": true,
    "gc": true,
    "gcDelay": "1h",
    "gcInterval": "24h"
  },
  "http": {
    "address": "0.0.0.0",
    "port": "5000",
    "realm": "zot",
    "auth": {
      "htpasswd": {
        "path": "/etc/zot/htpasswd"
      },
      "failDelay": 5
    }
  },
  "log": {
    "level": "info",
    "output": "/var/lib/registry/zot.log"
  },
  "extensions": {
    "search": {
      "enable": true
    },
    "ui": {
      "enable": true
    }
  }
}

各字段含义速览见第七章,完整参数族与生产模板见配套《Zot 2.1.22 配置参数详解》。

htpasswd(账号文件):zot 的 htpasswd 认证要求 bcrypt 散列($2b 前缀)。官方写法用 apache2-utils:htpasswd -bnB admin q1w2e3r4 > htpasswd;宿主机没有该工具时,实测可用 Python bcrypt 生成,效果相同:

$ python3 -c "import bcrypt; print('demo:' + bcrypt.hashpw(b'q1w2e3r4', bcrypt.gensalt(rounds=10, prefix=b'2b')).decode())"
demo:$2b$10$kmZ7BZolaoBpY7PjtaMSP.NGDr59OWAdL1b6HoNDkeunLkxL1xH7u

按同样方法生成 admin、reader 两行写入 htpasswd,本次实验实际使用的文件内容:

admin:$2b$10$h/AGifgcwXxamszKk.C/XOW77hiwyhbZosnnO0Ekdy1N3/C1tW0WK
reader:$2b$10$QzHAgTXxc8TjwAisCNDyc.GnO8sSBSqlw5g2w8ma7ro1OVyUGMV92

💡 技术拓展:htpasswd 文件格式是 用户名:口令散列,散列不是明文。Apache htpasswd 工具还支持 MD5($1)、SHA1({SHA})等算法,但 zot 只接受 bcrypt($2b),所以用 apache2-utils 时必须带 -B 参数,用 Python 时 prefix=b'2b'。示例口令 q1w2e3r4 仅为实验明文演示,生产请换强口令并只保留散列。

4.2 启动服务

cd /opt/data/quzuo/tmp/zot/stack && docker compose up -d

实测回显:

 Network zot-2.1.22-net Creating 
 Network zot-2.1.22-net Creating 
 Network zot-2.1.22-net Created 
 Network zot-2.1.22-net Created 
 Container zot-2-1-22-registry Creating 
 Container zot-2-1-22-registry Created 
 Container zot-2-1-22-registry Starting 
 Container zot-2-1-22-registry Started 

4.3 确认运行状态与启动日志

docker compose ps

实测回显:

NAME                  IMAGE                             COMMAND                  SERVICE   CREATED         STATUS         PORTS
zot-2-1-22-registry   ghcr.io/project-zot/zot:v2.1.22   "/usr/local/bin/zot-…"   zot       3 seconds ago   Up 2 seconds   0.0.0.0:8150->5000/tcp, [::]:8150->5000/tcp

本配置把 log.output 指向 /var/lib/registry/zot.log(即宿主机数据目录),日志不再走 stdout,所以 docker compose logs 是空的:

$ docker compose logs 2>&1 | head -8

启动过程看宿主机日志文件 /opt/data/quzuo/tmp/zot/data/zot.log,输出节选(实测,取启动关键四行):

{"time":"2026-11-01T07:47:45.950487247Z","level":"warn","message":"config dist-spec version differs from version actually used","config version":"1.1.0","supported version":"1.1.1","caller":"zotregistry.dev/zot/v2/pkg/cli/server/root.go:1300","func":"zotregistry.dev/zot/v2/pkg/cli/server.updateDistSpecVersion","goroutine":1}
{"time":"2026-11-01T07:47:45.953682196Z","level":"info","message":"loaded htpasswd file","htpasswd-file":"/etc/zot/htpasswd","users":2,"caller":"zotregistry.dev/zot/v2/pkg/api/htpasswd.go:70","func":"zotregistry.dev/zot/v2/pkg/api.(*HTPasswd).Reload","goroutine":1}
{"time":"2026-11-01T07:47:45.95487706Z","level":"info","message":"setting up ui routes","caller":"zotregistry.dev/zot/v2/pkg/extensions/extension_ui.go:72","func":"zotregistry.dev/zot/v2/pkg/extensions.SetupUIRoutes","goroutine":1}
{"time":"2026-11-01T07:47:45.954907633Z","level":"info","message":"finished setting up ui routes","caller":"zotregistry.dev/zot/v2/pkg/extensions/extension_ui.go:98","func":"zotregistry.dev/zot/v2/pkg/extensions.SetupUIRoutes","goroutine":1}

解读:首行 warn 是配置声明的 dist-spec 版本(1.1.0)低于二进制支持版本(1.1.1),zot 自动按 1.1.1 运行,无害(详见 FAQ 11.5);loaded htpasswd file 确认两个账号已加载;两行 ui routes 确认 Web 界面就绪。两行之间 zot 还会打印一整行 configuration settings 配置快照(单行 JSON,含全部生效参数),排障时可直接对照。

4.4 验证服务可达

$ curl -s -o /dev/null -w "%{http_code}" http://localhost:8150/v2/
401

$ curl -s -u admin:q1w2e3r4 http://localhost:8150/v2/ -w "\nHTTP %{http_code}\n"

HTTP 200

$ curl -s -u admin:q1w2e3r4 http://localhost:8150/v2/_catalog
{"repositories":["alpine"]}

$ curl -s http://localhost:8150/ | grep -o "<title>[^<]*</title>"
<title>zot OCI-native Container Image Registry</title>

$ curl -s -u admin:q1w2e3r4 http://localhost:8150/readyz -w " HTTP %{http_code}\n"
ok HTTP 200

解读:未带凭据访问 /v2/ 返回 401(Basic 质询,见 FAQ 11.4);带 admin 凭据返回 200;_catalog 此时已含 alpine(环境准备阶段先行推送的测试仓库,全新部署此处为空数组,第九章 8151 实例可对照);根路径返回 zot Web UI;/readyz 就绪探针返回 ok。

4.5 登录、推送与拉取闭环

$ echo q1w2e3r4 | docker login localhost:8150 -u admin --password-stdin

WARNING! Your credentials are stored unencrypted in '/root/.docker/config.json'.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/

Login Succeeded

$ docker tag busybox:latest localhost:8150/demo/busybox:v1

$ docker push localhost:8150/demo/busybox:v1
The push refers to repository [localhost:8150/demo/busybox]
37bb94b0940b: Unavailable
37bb94b0940b: Pushed
v1: digest: sha256:f97baa533a26513c453a362be331a43eb60214302f449c16571b23cea141dce4 size: 610

 Info -> Not all multiplatform-content is present and only the available single-platform image was pushed
         sha256:fd7dc98638c8e305f4dc34e979f1c0fdfdcaeb0fbf8fcff77ae834b6da3d7e6e -> sha256:f97baa533a26513c453a362be331a43eb60214302f449c16571b23cea141dce4

$ curl -s -u admin:q1w2e3r4 http://localhost:8150/v2/_catalog
{"repositories":["alpine","demo/busybox"]}

删掉本地副本再拉回,验证仓库侧数据完整:

$ docker rmi localhost:8150/demo/busybox:v1 && docker pull localhost:8150/demo/busybox:v1
v1: Pulling from demo/busybox
Digest: sha256:f97baa533a26513c453a362be331a43eb60214302f449c16571b23cea141dce4
Status: Downloaded newer image for localhost:8150/demo/busybox:v1
localhost:8150/demo/busybox:v1

推拉 digest 一致(sha256:f97baa533a26513c453a362be331a43eb60214302f449c16571b23cea141dce4),闭环完成。Info -> 两行是 docker 提示本地仅有单平台架构镜像,属正常。

4.6 用 zli 客户端复核

zli 是 zot 官方命令行客户端(发布页下载 zli-linux-amd64)。先把 8150 实例登记为命名端点,再查状态:

$ ./zli-linux-amd64 config add zot8150 http://localhost:8150
$ ./zli-linux-amd64 config list
local (default)	http://localhost:8080
zot8150		http://localhost:8150

$ ./zli-linux-amd64 status --config zot8150 -u admin:q1w2e3r4
Server Status: online
Server Version: v2.1.22
Dist Spec Version: 1.1.1
Built with: events, imagetrust, lint, metrics, mgmt, profile, scrub, search, sync, ui, userprefs

Built with 行直接列出该构建编译进去的全部扩展,与镜像选型(full 版)互相印证。zli 的搜索、CVE 报告、仓库管理等完整用法见配套《Zot 2.1.22 客户端操作与签名校验指南》。

五、数据持久化说明

全部仓库数据落在宿主机 /opt/data/quzuo/tmp/zot/data/(容器内 /var/lib/registry),zot 以标准 OCI image layout 存储,每个仓库一个目录:

$ ls -A /opt/data/quzuo/tmp/zot/data
alpine
cache.db
demo
meta.db
_sessions
zot.log

$ du -sh /opt/data/quzuo/tmp/zot/data
6.0M	/opt/data/quzuo/tmp/zot/data

$ cd /opt/data/quzuo/tmp/zot/data && find . -maxdepth 3 | sort
.
./alpine
./alpine/blobs
./alpine/blobs/sha256
./alpine/index.json
./alpine/oci-layout
./alpine/.uploads
./cache.db
./demo
./demo/busybox
./demo/busybox/blobs
./demo/busybox/index.json
./demo/busybox/oci-layout
./demo/busybox/.uploads
./meta.db
./_sessions
./zot.log
路径内容说明
<仓库名>/blobs/sha256/内容寻址 blob(层与 manifest)数据主体;dedupe 开启时相同层硬链接复用
<仓库名>/_manifests/、index.json、oci-layout标签索引与 OCI 布局标记标准 OCI layout 结构
<仓库名>/.uploads/分块上传暂存目录GC 会清理
cache.db、meta.dbzot 扩展元数据库(搜索/CVE 等)勿手工编辑
_sessions/Web UI 会话存储勿手工编辑
zot.log运行日志(log.output 落点)排障入口

重启验证数据不丢:

$ cd /opt/data/quzuo/tmp/zot/stack && docker compose restart
 Container zot-2-1-22-registry Restarting 
 Container zot-2-1-22-registry Started 

$ curl -s -u admin:q1w2e3r4 http://localhost:8150/v2/_catalog
{"repositories":["alpine","demo/busybox"]}

⚠️ 注意:备份 = 打包数据目录 + stack 下两个配置文件;恢复时整目录放回、重新 up -d 即可。因为落盘就是标准 OCI layout,理论上该目录也能被其他兼容实现直接服务。

六、环境变量说明

zot 镜像不读取任何运行时环境变量:编排文件没有 environment 段,全部行为由挂载的 config.json 驱动,容器入口命令固定 serve /etc/zot/config.json。这与 LinuxServer.io 系镜像靠 PUID/PGID/TZ 环境变量驱动部署的模型完全不同,对照如下:

需求zot 的做法
改端口、认证、扩展开关编辑 config.json 对应字段后 docker compose restart
改账号口令重新生成 htpasswd 后 docker compose restart(见 FAQ 11.2)
运行身份镜像内以 root 运行,靠挂载目录属主控制数据权限
时区无环境变量入口,日志时间戳为 UTC

七、配置参数速览

本部署 config.json 各字段作用速记(完整参数族、默认值与生产模板见配套《Zot 2.1.22 配置参数详解》):

字段本部署取值作用
distSpecVersion1.1.0声明兼容的 OCI Distribution Spec 版本,运行时自动按支持版本(1.1.1)执行
storage.rootDirectory/var/lib/registry数据唯一落盘点(已映射宿主机目录)
storage.dedupetrue相同内容层硬链接去重
storage.gc / gcDelay / gcIntervaltrue / 1h / 24h孤儿 blob 延迟回收、周期执行
http.address / port0.0.0.0 / 5000容器内监听,宿主机映射 8150
http.realmzotBasic 质询的 realm 名
http.auth.htpasswd.path/etc/zot/htpasswdbcrypt 账号文件
http.auth.failDelay5认证失败后延时 5 秒响应,抗口令爆破
log.level / outputinfo / /var/lib/registry/zot.log日志级别与落盘位置(导致 compose logs 为空)
extensions.search.enabletrueGraphQL 搜索 + CVE 扫描扩展
extensions.ui.enabletrueWeb 界面

八、配置文件挂载对照

宿主机路径容器内路径模式内容
/opt/data/quzuo/tmp/zot/stack/config.json/etc/zot/config.json只读服务全部行为配置
/opt/data/quzuo/tmp/zot/stack/htpasswd/etc/zot/htpasswd只读bcrypt 账号散列
/opt/data/quzuo/tmp/zot/data/var/lib/registry读写OCI layout 仓库数据 + 扩展元数据 + 日志

九、等价 docker run 命令

不用 Compose 时,一条 docker run 即可拉起等价实例。以下为本环境实测(8151 端口、独立空数据目录,验证后已删除):

$ mkdir -p /opt/data/quzuo/tmp/zot/data-runtest && docker run -d --name zot-2-1-22-cli-test -p 8151:5000 -v /opt/data/quzuo/tmp/zot/stack/config.json:/etc/zot/config.json:ro -v /opt/data/quzuo/tmp/zot/stack/htpasswd:/etc/zot/htpasswd:ro -v /opt/data/quzuo/tmp/zot/data-runtest:/var/lib/registry ghcr.io/project-zot/zot:v2.1.22
3b77bd192723b28dc0b94c452cd63f992b6e040c685974bb43f68c325212de40

$ docker ps --filter name=zot-2-1-22-cli-test --format '{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'
3b77bd192723	zot-2-1-22-cli-test	Up 8 seconds	0.0.0.0:8151->5000/tcp, [::]:8151->5000/tcp

$ curl -s http://localhost:8151/v2/ -o /dev/null -w "no-auth: HTTP %{http_code}\n"
no-auth: HTTP 401

$ curl -s -u admin:q1w2e3r4 http://localhost:8151/v2/ -w " HTTP %{http_code}\n"
 HTTP 200

$ curl -s -u admin:q1w2e3r4 http://localhost:8151/v2/_catalog -w " HTTP %{http_code}\n"
{"repositories":[]} HTTP 200

全新空数据目录的 _catalog 为空数组,与 4.4 节对照。验证完清理:

$ docker rm -f zot-2-1-22-cli-test && rm -rf /opt/data/quzuo/tmp/zot/data-runtest
zot-2-1-22-cli-test

十、常用运维命令

操作命令
启动 / 停止docker compose up -d / docker compose down
重启(改配置或账号后)docker compose restart
查看状态docker compose ps
看日志tail -f /opt/data/quzuo/tmp/zot/data/zot.log(compose logs 为空,见 FAQ 11.1)
健康检查curl -s -u admin:q1w2e3r4 http://localhost:8150/readyz
列仓库curl -s -u admin:q1w2e3r4 http://localhost:8150/v2/_catalog
列标签curl -s -u admin:q1w2e3r4 http://localhost:8150/v2/demo/busybox/tags/list
备份停服后打包 /opt/data/quzuo/tmp/zot/data 与 stack 下 config.json、htpasswd

docker compose down 实测回显(实验期间执行):

 Container zot-2-1-22-registry Stopping 
 Container zot-2-1-22-registry Stopped 
 Container zot-2-1-22-registry Removing 
 Container zot-2-1-22-registry Removed 
 Network zot-2.1.22-net Removing 
 Network zot-2.1.22-net Removed 

⚠️ 注意:zot 镜像是极简构建,容器内没有 shell 和任何工具,docker exec zot-2-1-22-registry ls 会报 exec: "ls": executable file not found in $PATH。运维一律从宿主机用 curl/zli 进行,或读挂载出来的日志文件。

十一、FAQ

11.1 docker compose logs 没有任何输出?

本配置 log.output 指向 /var/lib/registry/zot.log,日志写文件、不再写 stdout,compose logs 自然为空。直接看宿主机 /opt/data/quzuo/tmp/zot/data/zot.log;若想恢复 stdout 采集(如接日志平台),删掉 log 段的 output 字段重启即可。

11.2 修改 htpasswd 后新账号不生效?

本部署把 htpasswd 以单文件 bind mount 映射进容器(4.1 节编排文件 volumes 段的 ./htpasswd:/etc/zot/htpasswd:ro),inotify 事件不透传,zot 的 htpasswd watcher 收不到变更,改完必须 docker compose restart。实测:把 4.1 节生成的 demo 行追加进 htpasswd 后直接登录返回 401——

$ curl -s -u demo:q1w2e3r4 http://localhost:8150/v2/ -w " new-user: HTTP %{http_code}\n"
{"code":"UNAUTHORIZED","message":"authentication required","detail":{"description":"The access controller was unable to authenticate the client. Often this will be accompanied by a WWW-Authenticate HTTP response header indicating how to authenticate."}} new-user: HTTP 401

重启后新账号 200、错误口令 401:

$ cd /opt/data/quzuo/tmp/zot/stack && docker compose restart
 Container zot-2-1-22-registry Restarting 
 Container zot-2-1-22-registry Started 

$ curl -s -u demo:q1w2e3r4 http://localhost:8150/v2/ -o /dev/null -w " new-user: HTTP %{http_code}\n"
 new-user: HTTP 200

$ curl -s -u demo:wrongpass http://localhost:8150/v2/ -o /dev/null -w " wrong-pass: HTTP %{http_code}\n"
 wrong-pass: HTTP 401

另注意镜像内没有 htpasswd 工具,docker exec zot-2-1-22-registry htpasswd -bnB admin q1w2e3r4 报:

OCI runtime exec failed: exec failed: unable to start container process: exec: "htpasswd": executable file not found in $PATH

生成散列一律在宿主机做(apache2-utils 或 4.1 节 Python 一行命令)。

另注意这是挂载形态的限制而非 zot 本身:裸机/目录级挂载下 zot 对 htpasswd 有独立 watcher,账号增删免重启热加载(实测日志 htpasswd file changed, trying to reload config,见《Zot 2.1.22 配置参数详解》 3.3 节)。想避开本坑可改为目录级挂载:把 config.json 与 htpasswd 放进同一目录,volumes 写成 - ./etc:/etc/zot:ro,watcher 即可收到变更事件。

11.3 宿主机 8150 端口被占用怎么办?

ss -ltn | grep 8150 确认后,把编排文件里 "8150:5000" 改成其他空闲端口(容器内 5000 不动),docker compose up -d 重建;docker login、curl、zli 等命令中的端口同步替换。

11.4 push / 验证前客户端先报 401,正常吗?

正常。OCI Distribution Spec 的认证流程就是:匿名请求 → 服务端 401 并带 WWW-Authenticate: Basic realm="zot" 质询 → 客户端带凭据重试。docker 会自动完成这一跳,只要 Login Succeeded、push 出现 Pushed 即可。

11.5 启动日志首行 warn「config dist-spec version differs」要紧吗?

不要紧。配置声明 1.1.0、二进制实际支持 1.1.1,zot 自动按更高版本运行(4.6 节 zli status 回显 Dist Spec Version: 1.1.1)。介意告警就把 distSpecVersion 改为 1.1.1。

11.6 restart 后 compose logs 出现 error「failed to start controller, exiting」?

zot-2-1-22-registry  | {"time":"2026-11-01T08:13:56.708445987Z","level":"error","message":"failed to start controller, exiting","error":"http: Server closed","caller":"zotregistry.dev/zot/v2/pkg/cli/server/root.go:98","func":"zotregistry.dev/zot/v2/pkg/cli/server.newServeCmd.func1","goroutine":1}

这是旧进程收到停止信号、HTTP Server 被关闭时的收尾记录,属于重启噪音;随后新进程正常拉起(docker compose ps 状态 Up、/readyz 返回 ok 即可放心)。

十二、参考链接