Docker 基础体系 · 第 65/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 离线与受限网络:镜像同步、依赖缓存、签名和补丁
离线部署不是“把几条 docker pull 命令改成从 U 盘加载”。一个 Docker 构建或运行流程通常同时依赖四类数据:
- 基础镜像及其所有平台变体;
- Dockerfile 中执行的依赖下载,例如
apt、pip、npm、Go modules; - BuildKit 的构建缓存;
- 镜像的身份与安全证明,例如 digest、签名、SBOM 和 provenance。
只复制最终镜像,通常只能解决第 1 类问题;只复制 BuildKit cache,也不能自动获得第 2 类依赖;只验证 tag,更不能证明部署的内容没有变化。
本文以现代 Docker Engine、BuildKit 和 Compose 规范为基础,讨论 Linux 容器。Windows 容器具有不同的基础镜像、内核和隔离边界,不能直接套用本文的 Linux 结论。
一、先区分四种“离线”
“离线”不是单一状态。不同状态决定应采用不同的同步策略。
1. 受限网络
构建机或运行机仍然能够访问部分网络,但受到代理、白名单、带宽或 DNS 限制。例如:
- 只能访问企业 Registry;
- 只能通过 HTTP 代理访问公网;
- 只能访问内部软件仓库;
- 外部网络可以访问,但不允许任意域名。
这类环境可以使用 Registry mirror、代理、内部 PyPI/npm/apt 仓库以及预热缓存。
2. 构建离线、运行在线
构建环境无法访问公网,但运行环境可以从内部 Registry 拉取镜像。此时重点是把构建依赖和构建缓存带入构建环境。
3. 运行离线、构建在线
构建机能够联网,运行环境完全隔离。重点是导出完整镜像、镜像索引、签名和验证材料,并在隔离环境导入内部 Registry。
4. 完全物理隔离
构建机、Registry 和运行机都没有公网连接,只能通过受控介质传递文件。此时同步对象不能只是一个 tag,而应包括:
- 镜像 manifest 或 index;
- 所有镜像层;
- 必要的构建依赖;
- 签名与验证元数据;
- SBOM、漏洞扫描结果和发布清单;
- 可选的 BuildKit 外部缓存。
因此,离线交付的基本对象可以表示为:
其中:
- :镜像内容,包括 manifest、index 和 layers;
- :构建时依赖,例如
.deb、Python wheel、npm 包; - :构建缓存;
- :签名、证书和验证策略;
- :元数据,例如 SBOM、构建 provenance、版本清单。
只复制 ,并不等于复制了完整的可重建交付物 。
二、镜像的真实身份:Tag、Digest 和 Manifest
1. Tag 是可变名称
下面两个引用看起来都像固定版本:
nginx:1.27
nginx:latest
但 tag 只是 Registry 中的一个名称,可以在之后被重新指向另一个 manifest。即使仓库管理员没有恶意操作,重新构建、上游发布或镜像同步也可能改变 tag 所指向的内容。
因此:
docker pull nginx:1.27
表达的是“解析当前的 1.27”,不是“永远取得同一份字节”。
2. Digest 是内容地址
镜像 digest 通常形如:
sha256:2d429b9e73a...
它是 manifest 或 image index 的内容摘要。对同一个 registry 中的引用:
nginx@sha256:2d429b9e73a...
Docker 请求的是特定内容,而不是当前 tag。
获取实际 digest:
docker pull nginx:1.27
docker image inspect nginx:1.27 \
--format '{{json .RepoDigests}}'
可能得到:
["docker.io/library/nginx@sha256:..."]
这里需要注意两点:
- digest 绑定的是 manifest 或 index,不是简单地对单个 layer 文件做摘要;
- 同一组 layer 如果包装在不同 manifest 中,manifest digest 仍然可以不同。
3. 多架构镜像还存在 Image Index
一个 tag 可能指向一个 OCI Image Index 或 Docker manifest list。它包含:
linux/amd64 -> digest A
linux/arm64 -> digest B
客户端根据目标平台选择其中一项。因此,同一个:
alpine:3.20
在 linux/amd64 和 linux/arm64 上可能拉取不同内容。
可以检查远端索引:
docker buildx imagetools inspect alpine:3.20
输出会列出不同平台的 manifest digest。
离线同步时必须先确定部署平台:
docker image inspect myapp:1.0 \
--format 'OS={{.Os}} ARCH={{.Architecture}}'
如果需要支持多个平台,必须同步整个 index 及其引用的各平台 manifest 和 layers。只执行一次:
docker save myapp:1.0 -o myapp.tar
不能自动保证你已经取得了所有目标架构。docker save 保存的是本地镜像存储中已有的内容;本地只有 amd64 变体时,导出的也只有 amd64 内容。
三、两种镜像同步路径:Registry 复制和离线归档
1. Registry 复制适合持续同步
生产环境通常应优先使用内部 Registry,而不是让每台 Docker Engine 都通过 U 盘导入镜像。
一个典型数据流如下:
flowchart LR
O[外部 Registry] --> S[同步机]
S --> A[签名与清单校验]
A --> R[内部 Registry]
R --> B[离线构建机]
R --> N[离线运行节点]
同步机可以使用 Registry-aware 工具,例如 skopeo、crane 或专用 Registry 复制系统。它们能够直接处理远端 manifest 和 layers,避免必须先把镜像加载进 Docker Engine。
以 skopeo 为例:
skopeo copy \
docker://docker.io/library/alpine:3.20 \
docker://registry.intra.example/base/alpine:3.20
这条命令的前提是:
- 同步机能够访问源 Registry;
- 同步机能够认证内部 Registry;
- 目标仓库允许写入;
- 工具版本支持源和目标 Registry 的 manifest 格式。
然后在隔离环境验证:
docker pull registry.intra.example/base/alpine:3.20
docker image inspect registry.intra.example/base/alpine:3.20 \
--format '{{json .RepoDigests}}'
不要只比较源 tag 和目标 tag。 应在同步前记录源 digest,在同步后读取目标 digest,并比较二者:
源:
docker.io/library/alpine@sha256:SOURCE_DIGEST
目标:
registry.intra.example/base/alpine@sha256:TARGET_DIGEST
在没有重新压缩或转换 manifest 的情况下,二者应一致。若不一致,应确认:
- 同步工具是否转换了 manifest;
- 源仓库是否在同步期间移动了 tag;
- 目标 Registry 是否做了内容重写;
- 实际比较的是 index digest 还是某个平台 manifest digest。
2. docker save / docker load 适合一次性交付
没有 Registry 时,可以使用 Docker 镜像归档:
docker pull --platform linux/amd64 registry.intra.example/app/api:1.4.2
docker save \
--output api-1.4.2-amd64.tar \
registry.intra.example/app/api:1.4.2
sha256sum api-1.4.2-amd64.tar
将 tar 文件和校验清单传入离线环境后:
sha256sum -c api-1.4.2-amd64.tar.sha256
docker load --input api-1.4.2-amd64.tar
docker image ls
docker save 保存的是镜像及其 layer,docker load 将它们重新装入本地镜像存储。它不等价于完整复制 Registry:
- 不会替你复制 Registry 中的 tag 历史;
- 不会自动复制 OCI referrers 中的签名、SBOM 或 provenance;
- 不会把远端仓库的访问控制策略带过来;
- 不会把构建所需的 apt/npm/Python 依赖带过来。
导入后应通过 digest 验证,而不是仅看 tag:
docker image inspect registry.intra.example/app/api:1.4.2 \
--format '{{json .RepoDigests}}'
对于运行节点,通常还应把离线 tar 导入后重新打上内部仓库名并推送:
docker tag registry.intra.example/app/api:1.4.2 \
registry.airgap.local/app/api:1.4.2
docker push registry.airgap.local/app/api:1.4.2
此时要重新记录目标 Registry 返回的 digest。镜像内容通常不变,但目标引用已经发生变化。
四、Registry Mirror 能解决什么,不能解决什么
Docker Engine 可以配置 Registry mirror,使 Docker Hub 的镜像拉取经过内部缓存。例如 daemon 配置可能包含:
{
"registry-mirrors": [
"https://mirror.intra.example"
]
}
修改 /etc/docker/daemon.json 后通常需要重启 Docker:
sudo systemctl restart docker
docker info
docker info 的输出中应能看到 Registry Mirrors。
但 Registry mirror 的作用是镜像拉取缓存,不是通用网络代理。它不能自动缓存:
RUN apt-get update && apt-get install -y curl
RUN pip install -r requirements.txt
RUN npm ci
RUN go mod download
因为这些命令访问的分别可能是:
- Debian/Ubuntu 软件源;
- PyPI;
- npm Registry;
- Go module proxy。
如果 Dockerfile 中的 FROM 能从镜像 mirror 获取,但 RUN apt-get update 仍访问公网,构建依然会失败。
另外,pull-through cache 通常是“有请求才拉取”。在物理隔离环境中,第一次请求没有上游网络,不能把它当作已经预热的离线仓库。真正离线的内部 Registry 必须提前完成同步,或者在联网区生成并导入所需对象。
五、依赖缓存不是依赖归档
1. BuildKit layer cache 的基本机制
BuildKit 会根据 Dockerfile 指令、输入文件、基础镜像和相关构建参数判断某一层能否复用。例如:
FROM python:3.12-slim
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ /app/
如果只修改 src/,requirements.txt 没变,前面的依赖安装层通常可以复用。
但缓存命中要求构建机拥有对应的缓存记录和结果。换一台没有缓存的机器,Dockerfile 本身不会携带这些层。
可以显式导出本地 BuildKit 缓存:
docker buildx build \
--file Dockerfile \
--tag registry.intra.example/app/api:1.4.2 \
--cache-to type=local,dest=./buildkit-cache,mode=max \
--output type=docker \
.
在离线构建机导入:
docker buildx build \
--file Dockerfile \
--tag registry.airgap.local/app/api:1.4.2 \
--cache-from type=local,src=./buildkit-cache \
--output type=docker \
.
这里:
--cache-to将 BuildKit 可导出的缓存记录写到目录;mode=max通常比默认模式保存更多中间结果;--cache-from让后续构建尝试使用这些结果;--output type=docker将最终镜像加载到当前 Docker Engine。
缓存命中不是正确性的唯一来源。缓存可能被清理、失效或因 Dockerfile 改动而无法使用,所以构建必须在缓存完全不存在时仍有明确的依赖获取路径。
2. Cache mount 是构建机状态,不是天然的离线包仓库
常见写法是:
# syntax=docker/dockerfile:1
FROM python:3.12-slim
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
这个 cache mount 可以减少同一 BuildKit builder 上重复下载。但是它有一个重要边界:它主要是 builder 的持久化缓存状态,不应被误认为会自动随最终镜像或普通 cache export 成为可验证的依赖归档。
如果构建机损坏、换了 builder 或在另一台隔离机器上构建,/root/.cache/pip 不一定存在。依赖的离线交付应使用显式目录、内部包仓库或可验证的包归档。
六、把依赖变成可交付的离线输入
离线构建的可靠条件是:
Dockerfile 的每一个外部输入,要么已经进入构建上下文,要么能从离线可达的软件仓库获取,要么由已验证的构建缓存提供。
1. Python:wheelhouse + 哈希锁定
在线准备依赖目录:
python -m pip download \
--requirement requirements.txt \
--dest wheelhouse
这会下载 requirements 中的包及其依赖,但不保证一定得到适合目标平台的 wheel。若包含本地编译包,还应在目标 Linux、Python 版本和架构条件下准备相应 wheel。
requirements.txt 可以使用哈希约束,例如:
requests==2.32.3 \
--hash=sha256:HASH_OF_WHEEL_OR_SDIST
离线 Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY wheelhouse/ /wheelhouse/
COPY requirements.txt .
RUN python -m pip install \
--no-index \
--find-links=/wheelhouse \
--require-hashes \
-r requirements.txt
COPY src/ .
CMD ["python", "main.py"]
这几个选项分别保证不同事情:
--no-index:不访问 Python 包索引;--find-links=/wheelhouse:只从本地目录查找包;--require-hashes:要求 requirements 中的文件摘要匹配;COPY wheelhouse/:将依赖作为构建输入,而不是依赖一个可能不存在的缓存。
如果 wheelhouse 缺少某个传递依赖,构建会明确失败,而不是偷偷访问公网。这是期望的失败方式,因为它暴露了离线交付集合不完整的问题。
2. Debian/Ubuntu:软件包缓存必须包含传递依赖和索引
一个常见但不可靠的 Dockerfile 是:
RUN apt-get update && apt-get install -y curl
即便提前缓存了一个 curl.deb,也可能缺少 libcurl、证书包或其他依赖。
更稳妥的做法是在线环境准备完整软件包集合,并在离线环境配置本地源。例如在与目标镜像相同的发行版和架构环境中下载:
mkdir -p debs
apt-get download curl
但 apt-get download 通常只下载指定包,不会自动解决全部依赖。生产流程应使用能解析并下载依赖闭包的工具,或者维护一个内部 Debian/Ubuntu 仓库。把 .deb 放在目录中后可以生成简单的本地索引:
dpkg-scanpackages debs /dev/null | gzip -9c > debs/Packages.gz
Dockerfile:
FROM debian:bookworm-slim
COPY debs/ /opt/debs/
RUN printf 'deb [trusted=yes] file:/opt/debs .\n' \
> /etc/apt/sources.list.d/offline.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
这里的 trusted=yes 只适合说明本地实验,不适合生产。它关闭了 APT Release 签名信任的一部分保护。生产中应建立签名的内部 APT 仓库,并把内部仓库公钥以受控方式放入基础镜像,再使用正常的签名校验。
APT 还具有发行版和架构约束:
- Debian bookworm 的包不应随意用于 Ubuntu;
- amd64 包不能直接替代 arm64 包;
- 仓库索引和包文件必须来自兼容的发行版快照;
apt-get update得到的索引也属于构建输入,应记录其来源和时间。
3. npm、Go 和其他生态
npm 可以使用预先填充的缓存,但缓存目录格式和 npm 版本有关;更可控的方式是搭建内部 npm Registry,或准备离线包集合并让安装命令严格禁止联网。Go modules 通常应使用内部 module proxy 或提前填充 GOMODCACHE,并保留 go.sum。
这些工具有共同规律:
“下载过一次”只是缓存状态,不是交付保证。
七、构建上下文、.dockerignore 和离线文件
离线依赖经常通过 Docker build context 传入:
project/
├── Dockerfile
├── requirements.txt
├── wheelhouse/
└── src/
执行:
docker buildx build --load -t app:offline ./project
BuildKit 会把上下文发送给 builder。若 .dockerignore 中写了:
wheelhouse/
那么 Dockerfile 的:
COPY wheelhouse/ /wheelhouse/
会失败或复制不到文件。
因此,离线构建问题中出现“文件明明在目录里,但构建看不到”的第一诊断点就是:
cat .dockerignore
还要注意构建上下文不应无差别包含整个缓存目录。大型缓存会:
- 增加传输时间;
- 改变上下文摘要;
- 导致无关修改使缓存失效;
- 增加供应链审查范围。
依赖归档应使用明确目录和清单,而不是把开发机的整个 home 目录复制给构建器。
八、签名、digest 和校验和是三种不同保证
1. 校验和保证文件传输未被意外改变
例如:
sha256sum image.tar > image.tar.sha256
sha256sum -c image.tar.sha256
它能验证 tar 文件在传输后仍与发布时相同,但前提是 .sha256 文件本身通过可信渠道获得。若攻击者可以同时替换 tar 和校验文件,校验没有安全意义。
2. Digest 保证引用的内容身份
镜像 digest 适合写进部署清单:
services:
api:
image: registry.airgap.local/app/api@sha256:IMAGE_DIGEST
这使 Compose 不再依赖可变 tag。仍然需要确保 digest 来源可信,例如由发布系统签名或由受控清单发布。
3. 签名把身份和策略绑定到内容
镜像签名通常表达类似命题:
某个可信主体批准了 digest 为
D的镜像用于某个用途。
签名验证至少涉及:
- 被签名的对象到底是 manifest 还是 index;
- 签名者身份;
- 信任根或证书链;
- 签名是否覆盖目标 digest;
- 签名是否过期或被撤销;
- 当前部署策略是否允许该签名者。
因此,“镜像能运行”不等于“镜像已签名”,“有签名”也不等于“当前环境信任该签名者”。
4. Docker Content Trust 与 OCI 签名工具不能混为一谈
Docker Content Trust 基于 Notary v1 的签名和元数据体系,常见使用方式是:
export DOCKER_CONTENT_TRUST=1
docker pull registry.example.com/app/api:1.4.2
它对支持该机制的 Docker Registry 和 tag 签名流程有特定要求。Notary v1、Docker Content Trust 与现代 OCI artifact 生态并不是同一个签名格式。企业应先确定使用哪一种信任系统,而不是在不同工具之间假设签名可以互认。
现代供应链也常使用 Cosign 等 OCI 签名工具。例如,签名通常与镜像 digest 关联,而不是与 tag 关联:
cosign sign \
--key cosign.key \
registry.airgap.local/app/api@sha256:IMAGE_DIGEST
验证:
cosign verify \
--key cosign.pub \
registry.airgap.local/app/api@sha256:IMAGE_DIGEST
命令和存储方式取决于 Cosign 版本、密钥模式和 Registry 对 OCI referrers 的支持。不能只复制镜像 layers 后期待签名自动存在,因为签名可能作为独立 OCI artifact 存储在 Registry 中。
使用复制工具时,应确认是否复制了:
- image index;
- 签名对象;
- SBOM;
- provenance;
- OCI referrers 关系。
若工具只执行普通镜像复制,往往只复制主镜像,不复制附属 artifact。离线交付应把“镜像 digest + 签名 digest + SBOM digest”一起写入发布清单,并在目标 Registry 中分别验证。
九、签名验证的完整流程
一个可审计的离线流程可以是:
在线发布区
docker buildx build \
--platform linux/amd64 \
--push \
--tag registry.online.example/app/api:1.4.2 \
.
docker buildx imagetools inspect \
registry.online.example/app/api:1.4.2
从输出中记录:
registry.online.example/app/api@sha256:IMAGE_DIGEST
然后对 digest 签名:
cosign sign --key cosign.key \
registry.online.example/app/api@sha256:IMAGE_DIGEST
再生成 SBOM,并将其与该 digest 关联。最后导出一份不可随意修改的发布清单:
application: api
version: 1.4.2
platform: linux/amd64
image: registry.online.example/app/api@sha256:IMAGE_DIGEST
signature: SIGNATURE_REFERENCE_OR_DIGEST
sbom: SBOM_REFERENCE_OR_DIGEST
builder: BUILDER_ID
source_revision: GIT_COMMIT
传输区
复制:
- 镜像本体;
- 签名和其他 referrer artifact;
- 发布清单;
- 签名公钥或验证证书;
- 每个文件的外层校验和。
离线区
先校验传输文件,再同步或加载到内部 Registry:
sha256sum -c transfer.sha256
随后验证镜像身份:
docker pull registry.airgap.local/app/api@sha256:IMAGE_DIGEST
docker image inspect \
registry.airgap.local/app/api@sha256:IMAGE_DIGEST
最后验证签名:
cosign verify \
--key cosign.pub \
registry.airgap.local/app/api@sha256:IMAGE_DIGEST
如果验证失败,不应通过改成 tag、关闭校验或重新签名来“修复”流程。应区分失败原因:
- digest 不一致:镜像同步错误或引用解析错误;
- 签名找不到:referrer 未复制或目标 Registry 不支持对应索引;
- 签名不匹配:签名针对另一 digest;
- 公钥不可信:信任根没有正确分发;
- 证书策略失败:签名者不符合环境策略。
十、Compose 中的离线部署
Compose 文件可以使用内部 Registry 和 digest:
services:
api:
image: registry.airgap.local/app/api@sha256:IMAGE_DIGEST
platform: linux/amd64
ports:
- "8080:8080"
部署前检查:
docker compose config
docker compose pull
docker compose up -d
docker compose config 用于确认变量替换、合并文件和最终配置;它不验证镜像是否已存在,也不验证签名。
在完全离线环境中,如果镜像已经通过 docker load 导入,则可以跳过 pull,但 Compose 中的镜像引用必须能匹配本地镜像。使用 digest 时尤其要检查本地镜像是否带有对应的 repo digest:
docker image inspect \
registry.airgap.local/app/api@sha256:IMAGE_DIGEST
如果本地只存在:
api:1.4.2
而 Compose 要求:
registry.airgap.local/app/api@sha256:...
Docker 不会仅因为 layer 内容相同就自动把二者视为同一个引用。可以在导入后重新打 tag,或更稳定地先推送到内部 Registry,再让 Compose 从内部 Registry 使用 digest。
十一、补丁不是在运行容器里执行 apt upgrade
1. 容器补丁的正确对象是镜像构建
正在运行的容器是由镜像创建的可写容器层。直接进入容器执行:
docker exec -it api sh
apt-get update
apt-get upgrade
即使命令成功,也只修改当前容器的可写层:
- 新建容器不会继承这些修改;
- 容器删除后修改丢失;
- 运行节点之间状态不一致;
- 无法可靠对应源码、镜像 digest 和漏洞扫描结果。
正确流程是更新 Dockerfile 或依赖锁文件,重新构建新镜像,测试后发布新的 immutable digest:
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
然后:
docker buildx build \
--tag registry.airgap.local/app/api:1.4.3 \
--push \
.
部署清单改为新 digest,而不是把旧 tag 重新指向新内容后不留记录。
2. 基础镜像补丁会改变上层构建结果
假设:
FROM debian:bookworm-slim
RUN apt-get install -y curl
即使 Dockerfile 不变,debian:bookworm-slim 这个 tag 重新指向新 digest 后,后续构建的基础输入也可能改变。对于可重复构建,应固定基础镜像:
FROM debian:bookworm-slim@sha256:BASE_DIGEST
补丁发布时显式修改 BASE_DIGEST,这样差异是可审查的。
3. 什么时候可以只重建,什么时候必须重新下载
如果离线构建区有可靠的 BuildKit cache,并且新基础镜像及其补丁层已经导入,构建可能只需复用缓存。但以下情况会迫使重新取得数据:
- 基础镜像 digest 变化;
apt索引或包版本变化;requirements.txt锁文件变化;- 缓存被清理;
- Dockerfile 指令或构建参数变化;
- 目标架构变化。
因此,补丁流程必须准备“缓存全部失效”的路径。否则第一次缓存清理就会暴露离线环境根本没有依赖源的问题。
十二、补丁发布的一个完整算例
假设生产环境当前运行:
registry.airgap.local/app/api@sha256:OLD_DIGEST
安全团队要求升级 OpenSSL。发布区执行:
- 更新基础镜像 digest 或内部 APT 仓库快照;
- 重新构建;
- 运行测试;
- 生成 SBOM;
- 扫描新镜像;
- 对新 digest 签名;
- 将镜像及相关 artifact 同步到离线 Registry;
- 在离线区验证;
- 更新 Compose 或部署清单。
离线验证:
docker pull registry.airgap.local/app/api@sha256:NEW_DIGEST
cosign verify \
--key cosign.pub \
registry.airgap.local/app/api@sha256:NEW_DIGEST
docker run --rm \
registry.airgap.local/app/api@sha256:NEW_DIGEST \
/app/healthcheck
确认应用健康后,再修改 Compose:
services:
api:
image: registry.airgap.local/app/api@sha256:NEW_DIGEST
执行更新:
docker compose up -d --no-build api
docker compose ps
docker compose logs --tail=100 api
这里的 --no-build 表示部署阶段不尝试本地构建;如果镜像不在本地或内部 Registry 中,命令应失败,而不是隐式访问公网。
回滚只需把引用改回已验证的旧 digest:
image: registry.airgap.local/app/api@sha256:OLD_DIGEST
然后:
docker compose up -d --no-build api
这要求旧镜像仍保留在内部 Registry 或节点本地。因此清理策略不能只按 tag 或最近访问时间删除,还应保留当前版本和可回滚版本。
十三、故障路径与诊断顺序
1. pull 失败,但 mirror 看起来正常
先判断失败的是基础镜像还是构建命令中的其他下载:
docker pull registry.intra.example/base/alpine:3.20
docker buildx build --progress=plain .
如果 FROM 成功而 RUN apt-get update 失败,问题不在 Docker Registry mirror,而在 APT 源、代理或离线包仓库。
2. 缓存显示存在但没有命中
使用普通日志观察 BuildKit:
docker buildx build --progress=plain .
常见原因:
- Dockerfile 中
COPY . .太早,源码变化导致后续层失效; requirements.txt实际发生变化;- 基础镜像 digest 变化;
- cache-from 指向错误目录;
- builder 实例不是导出缓存时使用的实例;
- 目标平台不同。
可以查看 builder:
docker buildx ls
docker buildx inspect --bootstrap
缓存只能加速,不能作为唯一依赖来源。验证方法是复制一份环境,删除或不提供 cache,确认仍能通过离线仓库或本地依赖目录完成构建。
3. docker load 成功但 Compose 找不到镜像
检查本地引用:
docker image ls
docker image inspect api:1.4.2
如果 Compose 使用的是内部 Registry 全名,而导入后的镜像只有短 tag,应重新打 tag:
docker tag api:1.4.2 registry.airgap.local/app/api:1.4.2
若 Compose 使用 digest,则还必须确认对应 repo digest 存在,不能只比较镜像 ID。
4. 签名验证找不到签名
应先确认验证的对象:
registry.airgap.local/app/api@sha256:D
再检查同步流程是否只复制了主镜像。主镜像 layer 存在而签名不存在,通常说明 OCI referrer artifact 没有复制,或目标 Registry/复制工具对相关 artifact 支持不足。
不要对新 tag 直接验证并据此推断安全性。签名通常绑定 digest;tag 被重新指向后,原签名不会自动覆盖新内容。
5. 离线构建执行了网络访问
可以在隔离构建网络中显式阻断出口,使用:
docker buildx build --network=none .
但这会影响需要本地仓库访问的步骤;它适合验证 Dockerfile 是否意外依赖公网,不应被误解为依赖自动变得可用。
更实用的验证方式是:
- 构建节点没有默认路由;
- DNS 不解析公网域名;
- 只允许访问内部 Registry 和内部包仓库;
- 构建日志中不存在外部下载;
- 删除 BuildKit cache 后重新构建仍成功。
十四、清理和保留策略
镜像、layer、BuildKit cache、签名 artifact 和包缓存是不同类型的对象,不能用一个清理命令解决所有问题。
Docker 镜像清理
docker image prune
它主要清理悬空镜像。更激进的:
docker image prune -a
可能删除没有容器引用的旧镜像,从而破坏离线回滚能力。
BuildKit 缓存清理
docker buildx prune
这可能删除后续离线重建需要的缓存。离线环境中应先确认:
- 完整依赖归档已经存在;
- 内部包仓库可用;
- 当前版本和回滚版本镜像已推送;
- 重新构建测试已经通过。
Registry 清理
Registry 的垃圾回收通常需要先删除 manifest 引用,再执行垃圾回收;具体行为取决于 Registry 实现和存储后端。只删除 tag 不一定立即删除 layer,因为同一 layer 可能仍被其他 manifest 引用。
清理前应根据 digest 建立保留集合:
当前生产 digest
最近一个可回滚 digest
审计要求保留的历史 digest
签名、SBOM 和 provenance 对应 artifact
只以 latest 或时间标签为依据清理,无法可靠判断回滚和审计需求。
十五、一个可执行的离线交付目录
可以把一次发布组织成如下结构:
release-api-1.4.2/
├── manifest.txt
├── transfer.sha256
├── images/
│ └── api-1.4.2-amd64.tar
├── buildkit-cache/
├── wheelhouse/
├── debs/
├── sbom/
│ └── api-1.4.2.spdx.json
├── signatures/
│ └── api-signature.bundle
└── keys/
└── cosign.pub
其中 manifest.txt 至少记录:
name=api
version=1.4.2
platform=linux/amd64
image_digest=sha256:...
base_image_digest=sha256:...
source_revision=...
dockerfile_sha256=...
sbom_sha256=...
signature_reference=...
发布检查可以用脚本实现:
set -eu
sha256sum -c transfer.sha256
docker load --input images/api-1.4.2-amd64.tar
docker image inspect \
registry.airgap.local/app/api@sha256:IMAGE_DIGEST >/dev/null
cosign verify \
--key keys/cosign.pub \
registry.airgap.local/app/api@sha256:IMAGE_DIGEST
docker run --rm \
registry.airgap.local/app/api@sha256:IMAGE_DIGEST \
/app/healthcheck
示例中的 IMAGE_DIGEST 必须替换为发布清单中的真实 digest;生产脚本应从已校验的清单读取,而不是让操作员手工输入。脚本失败应停止后续部署,不能用 || true 把验证错误隐藏掉。
十六、必须保留的边界
最后需要明确几个容易混淆的边界:
- 镜像同步不等于依赖同步:Registry 只解决镜像对象,不自动提供 APT、PyPI、npm 或 Go module;
- 缓存不等于归档:BuildKit cache 可以失效或被清理,离线依赖应有显式来源;
- tag 不等于版本不可变:部署和审计应使用 digest;
- digest 不等于签名:digest 能识别内容,但不能说明谁批准了它;
- 签名不等于漏洞修复:签名证明发布者批准了内容,不代表内容没有漏洞;
- 补丁不等于修改运行容器:补丁应进入新的镜像构建和新的 digest;
docker save不等于复制整个 Registry:签名、SBOM、索引和 referrer 可能需要单独同步;- Linux 容器不等于完整虚拟机:容器共享宿主机 Linux 内核。镜像离线且完整,并不意味着宿主机内核、CPU 指令集、设备驱动和安全策略已经满足运行要求。
一个可审计、可恢复的离线交付,应能回答四个问题:
- 运行的到底是哪一个 digest?
- 该 digest 所需的所有镜像和依赖是否已完整进入隔离环境?
- 谁签署了它,离线环境依据什么信任该签名?
- 补丁发布后,旧版本是否仍可验证并回滚?
只要这四个问题都有可验证答案,离线部署就不再是一次性的文件搬运,而成为具有身份、依赖闭包、验证和恢复路径的发布流程。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker Engine 升级:兼容、存储驱动、API、灰度、回滚和验证
- 下一篇:Docker 磁盘治理:Layer、Build Cache、Volume、日志和安全清理
- 延伸:Docker 可重复依赖安装:锁文件、镜像源、缓存、校验和离线
- 延伸:Docker Registry 与镜像分发:Tag、Digest、认证、缓存和清理
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论