在离线 / Air-Gapped 环境中构建带 Web UI 的 Zot
在完全离线(Air-Gapped)的机器上从源码构建 Zot 时,一个常见问题是:Zot 的构建过程会尝试从 GitHub 下载或构建 ZUI(Zot Web UI)。如果机器无法访问 GitHub,make 就会失败。
本文记录一种简单、可重复的做法:提前准备 ZUI 和 Go 依赖,在离线环境中构建 Zot 二进制文件,再直接把已经构建好的二进制打包成 Docker 镜像。
1. 问题现象
直接执行:
make
可能看到类似错误:
curl: Failed to connect to github.com
Cloning into 'zui'...
fatal: unable to access 'https://github.com/project-zot/zui.git/'
原因是 Zot 的 Makefile 在启用 ui extension 时,需要准备 ZUI。如果预编译的 UI 不存在,它会尝试:
- 从 GitHub Release 下载
zui.tgz; - 下载失败后,
git cloneZUI; - 执行
npm install和npm run build。
这些操作都需要网络。
2. 找到 Zot 对应的 ZUI 版本
不要自己猜 ZUI 版本。Zot 当前版本对应的 ZUI ref 已经写在 Makefile 中。
相关变量集中在 Makefile 顶部。在 Zot 源码目录执行:
grep -n '^ZUI' Makefile
输出类似:
29:ZUI_BUILD_PATH := ""
30:ZUI_VERSION := commit-a7feb46
31:ZUI_REPO_OWNER := project-zot
32:ZUI_REPO_NAME := zui
其中 ZUI_VERSION 就是要找的 ref。只取它的值:
sed -n 's/^ZUI_VERSION[[:space:]]*:=[[:space:]]*//p' Makefile
更方便的做法是直接存进 shell 变量,后面的下载和 clone 都复用它:
ZUI_VERSION=$(sed -n 's/^ZUI_VERSION[[:space:]]*:=[[:space:]]*//p' Makefile)
echo "$ZUI_VERSION"
也可以直接观察 make 输出。
例如,本文遇到的 Zot 版本使用:
commit-a7feb46
构建日志中可以看到:
https://github.com/project-zot/zui/releases/download/commit-a7feb46/zui.tgz
以及:
git clone --depth=1 --branch "commit-a7feb46" \
https://github.com/project-zot/zui.git
因此,这个 Zot checkout 对应的 ZUI ref 就是:
commit-a7feb46
每次切换 Zot tag/commit 后,都应该重新检查 Makefile,而不是一直使用这个示例值。
3. 在联网机器上准备 ZUI
有两种方法。
方法 A:直接使用 Zot 指定的预编译 ZUI
这是更推荐的方法,因为它与 Zot Makefile 默认使用的 artifact 完全一致。
例如:
curl -fL \
"https://github.com/project-zot/zui/releases/download/$ZUI_VERSION/zui.tgz" \
-o zui.tgz
解压:
mkdir -p zui-build
tar xzf zui.tgz -C zui-build
检查内容:
find zui-build -maxdepth 2 -type f | head
然后把这个目录复制到离线机器。
方法 B:自己构建 ZUI
如果没有对应的预编译 artifact,可以从源码构建:
git clone \
--depth=1 \
--branch "$ZUI_VERSION" \
https://github.com/project-zot/zui.git
cd zui
npm install
npm run build
构建结果通常位于:
zui/build/
把这个 build/ 目录复制到离线机器即可。
4. 准备 Zot 的 Go 依赖
如果离线机器也不能访问 Go module proxy,需要提前 vendor Go dependencies。
在联网机器上进入对应版本的 Zot:
git clone https://github.com/project-zot/zot.git
cd zot
获取 tags:
git fetch --tags
切换到需要构建的版本,例如:
git checkout <zot-tag>
然后:
go mod download
go mod vendor
此时源码目录中应该出现:
vendor/
将完整 Zot 源码目录(包括 vendor/)复制到离线机器。
5. 在离线机器上构建 Zot + UI
假设准备好的 ZUI 位于:
/opt/zui/build
进入 Zot 源码:
cd zot
强制 Go 不访问网络:
export GOPROXY=off
export GOSUMDB=off
export GOFLAGS=-mod=vendor
然后执行:
make binary \
ZUI_BUILD_PATH=/opt/zui/build
关键参数是:
ZUI_BUILD_PATH=/opt/zui/build
设置以后,Zot Makefile 会直接使用本地已经准备好的 ZUI,而不是尝试访问 GitHub。
如果目标是 Linux AMD64,也可以显式指定:
make binary \
OS=linux \
ARCH=amd64 \
ZUI_BUILD_PATH=/opt/zui/build
构建完成后,二进制文件通常在:
bin/
例如:
bin/zot-linux-amd64
6. 验证二进制
先确认 Zot 可以运行:
./bin/zot-linux-amd64 --help
然后检查是否为静态链接:
file bin/zot-linux-amd64
以及:
ldd bin/zot-linux-amd64
如果是完全静态链接,ldd 通常会显示:
not a dynamic executable
7. 用已经构建好的 Zot 二进制制作 Docker 镜像
既然 Zot 已经构建完成,就没有必要在 Dockerfile 中再次安装 Go、Node.js、npm,然后重新编译 Zot。
如果 Zot binary 是静态链接,可以使用非常简单的 Dockerfile:
FROM scratch
COPY bin/zot-linux-amd64 /usr/bin/zot
EXPOSE 5000
ENTRYPOINT ["/usr/bin/zot"]
CMD ["serve", "/etc/zot/config.json"]
保存为:
Dockerfile.runtime
然后:
docker build \
-f Dockerfile.runtime \
-t zot:local .
因为使用 FROM scratch,Docker 构建过程甚至不需要下载 Alpine、Ubuntu 等基础镜像。
注意:如果 Zot binary 不是静态链接,就不能直接使用
scratch,需要选择包含相应运行库的基础镜像。
8. ZUI 需要复制进 Docker 镜像吗?
通常不需要。
在执行:
make binary ZUI_BUILD_PATH=/opt/zui/build
时,ZUI 已经作为 Zot UI extension 构建进最终程序所需的资源中。
因此 runtime image 不需要再次执行:
COPY zui/build ...
只需要复制最终 Zot binary。
9. 导出 Docker 镜像
如果 Docker image 是在另外一台构建机器上生成的,可以导出:
docker save zot:local | gzip > zot-local.tar.gz
把 zot-local.tar.gz 复制到最终离线服务器,然后执行:
gunzip -c zot-local.tar.gz | docker load
检查:
docker images zot
10. 推荐的完整流程
整个过程可以简化为:
flowchart TD
subgraph ONLINE["联网机器"]
ZS["Zot source"]
V["vendor/<br/>go mod vendor"]
REF["从 Makefile 找到<br/>对应的 ZUI ref"]
TGZ["下载 zui.tgz"]
NPM["npm run build"]
ZB["zui/build/"]
ZS --> V
REF --> TGZ --> ZB
REF --> NPM --> ZB
end
subgraph OFFLINE["离线构建机器"]
MK["make binary ZUI_BUILD_PATH=..."]
BIN["zot-linux-amd64"]
DB["docker build"]
IMG["zot:local"]
MK --> BIN --> DB --> IMG
end
V -->|复制| MK
ZB -->|复制| MK
11. 最核心的命令
如果所有依赖已经准备好,真正的离线构建其实只有:
GOPROXY=off \
GOSUMDB=off \
GOFLAGS=-mod=vendor \
make binary \
ZUI_BUILD_PATH=/opt/zui/build
然后用已经生成的 binary 构建镜像:
docker build \
-f Dockerfile.runtime \
-t zot:local .
总结
Air-Gapped 环境中构建 Zot 的关键不是修改 Zot 源码,而是提前把所有需要网络获取的依赖变成本地输入:
Go modules → vendor/
Zot Web UI → zui/build/
其中 ZUI 版本不要手工猜测,而应该从当前 Zot checkout 的 Makefile 中获取。
最后,Zot 编译完成后,直接使用已经生成的 binary 构建 runtime Docker image。这样可以避免在 Docker build 阶段再次引入 Go、Node.js、npm、GitHub 等网络和构建依赖,使整个离线构建过程更简单、更稳定,也更容易重复。