DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

Docker COPY 使用通配符后,符号链接变成普通文件

BuildKit 直接选中的源链接会被解引用。改为复制专用父目录,保留目录内部链接及目标布局,并检查多阶段产物、绝对链接和最终文件类型。

源目录中的 current -> data.txt 是符号链接,Docker 镜像构建成功后,ls -l 却显示 current 成了普通文件。若 COPY 写的是 payload/*,需要检查它是否把链接本身选成了一个独立的复制源。

在这里讨论的 BuildKit Linux 镜像构建中,直接选中的源链接会被解引用,复制的是目标内容。把父目录作为复制源,目录内部的链接则可以作为链接保留。COPY payload/* /opt/payload/ 和 COPY payload/ /opt/payload/ 因此不等价。

本文处理上下文内已有文件链接的保留,不用符号链接读取构建上下文外的文件,也不把这一规则推广到 Windows 容器或其他构建器。

先确认链接在哪一步变成了文件

假设测试上下文如下,data.txt 只放不含凭据的测试文本:

project/
├── Dockerfile
└── payload/
    ├── data.txt
    └── current -> data.txt

在 Linux 上检查源目录:

ls -l payload/current payload/data.txt
readlink payload/current

readlink 应读到链接中保存的路径 data.txt。如果这里 current 已经是普通文件,就要向上检查代码检出、打包或复制输入的步骤;修改 Dockerfile 不能恢复一个进入上下文前就已经丢失的链接。

再检查 .dockerignore 和实际构建目录。链接存在但目标文件被排除,与链接变成普通文件是两种现象。构建阶段和运行阶段也要分开:运行时另一个挂载可能遮盖镜像目录,不能只凭挂载后的结果推断 COPY 的产物。

复制专用目录,而不是用通配符逐个选文件

在这个最小测试项目里,Dockerfile 可以写成:

# syntax=docker/dockerfile:1
FROM alpine:3.23
COPY payload/ /opt/payload/

对已有项目,只修改需要保留链接的 COPY,不必替换原基础镜像。这里选中的是 payload 目录,目录内的 current 与 data.txt 一起复制,保留它们之间的相对关系。

源路径末尾的 / 不是一个“保留链接”开关。对目录源来说,COPY payload /opt/payload/ 与这里的目录复制语义相同;关键是去掉让各个文件成为独立源的 *。仅把目标路径补上斜杠,却继续使用 payload/*,不能解决链接被解引用的问题。

目录源复制的是目录内容,不额外生成一层 payload。因此这条指令的结果是 /opt/payload/current,不是 /opt/payload/payload/current。不要为得到需要的布局直接换成 COPY . /opt/payload/,那会扩大范围,把项目中其他文件也带进镜像。把待交付文件整理到专用目录更容易检查。

多阶段复制也要保留整组相对关系

如果这些文件来自构建阶段,应让构建输出本来就位于一个专用目录,再整体复制该目录。例如:

COPY --from=build /out/payload/ /opt/payload/

这是需要合并进已有多阶段 Dockerfile 的片段,要求 build 阶段和 /out/payload/ 已存在。不要为解决链接问题把整个系统库目录复制到最终镜像;那可能引入不匹配的运行库或不需要的文件。

链接链中的每一层和最终目标都要随需要的目录布局保留。例如 libdemo.so -> libdemo.so.1 -> libdemo.so.1.0,只留下第一个链接仍然可能无法读取。若构建工具只提供散落文件,应先按该工具支持的方式组织完整产物目录,并检查整理步骤没有提前解引用链接。

绝对链接也需要单独核对:current -> /build/data.txt 复制后仍指向 /build/data.txt,不会自动改指 /opt/payload/data.txt。父目录复制保留的是链接关系的表示,不负责把不适合目标镜像的路径改写成正确布局。

不要把 COPY --link 当成保留文件符号链接的选项。它处理的是复制层与此前镜像状态的关系,另有目标路径限制,并不是这里需要的“不要跟随源链接”。

同时检查文件类型和可读目标

在独立测试项目中构建新标签,然后验证镜像里的结果:

docker build -t symlink-demo:check .
docker run --rm --network none symlink-demo:check \
    sh -c 'test -L /opt/payload/current && readlink /opt/payload/current && test -f /opt/payload/current'

这些命令只用于前面的无敏感数据示例,不是要求在生产主机执行。test -L 检查对象是链接;readlink 检查保存的路径;test -f 会沿链接判断目标是不是普通文件。三项一起看,可以区分“复制成了普通文件”和“保留了链接但目标丢失”。最后一项只适用于这里链接到普通文件的示例。

现有镜像如果没有 shell、test 或 readlink,应采用适合该镜像的离线文件检查方式,不要为排错把调试工具永久加进运行镜像。对动态库还需按项目原有测试验证应用能加载正确版本;链接结构正确不等于 ABI 一定兼容。

保留原 Dockerfile 和已验证镜像,用新的候选标签完成检查后再发布。若扩大目录复制后带入了额外文件,先收窄产物目录,不能把“链接恢复”作为唯一通过条件。需要回退时恢复 COPY 改动与旧镜像,不删除卷、不清空构建缓存,也不直接在运行容器里重建链接来代替修复构建流程。