多阶段 Docker 镜像仍含 .git:.dockerignore 不过滤 COPY --from 的阶段产物
构建阶段生成或下载的文件不受本地上下文忽略规则过滤;将发布产物放到独立输出目录,再从该目录复制,检查最终镜像而非运行挂载。
.dockerignore 已写入 .git,最终镜像却仍带着仓库历史。如果仓库是在构建阶段通过 RUN 下载的,随后用 COPY --from=builder /source/ /app/ 整体搬进最终阶段,本地忽略规则没有过滤这些阶段产物。
.dockerignore 处理的是构建上下文中的输入。COPY --from 可以读取其他阶段或镜像的文件系统,不能把上下文忽略规则理解成每次复制都会自动执行的发布过滤器。
确认多出来的文件在哪里产生
沿 Dockerfile 查 .git 在哪一步产生。它是从本地上下文复制进去的,还是在 RUN git clone 等构建步骤中才产生的?前一种要核对实际上下文和生效的忽略文件;后一种应检查跨阶段复制的源目录。
同时区分镜像内容和运行挂载。容器把宿主机源码目录挂到 /app 后,看到的 .git 可能来自挂载,不能用它证明镜像复制规则失效。排查时查看容器挂载定义,并用项目已有的离线镜像检查工具核对最终产物;不要为查看内容执行不受信任的镜像入口。
如果源阶段本来就没有 .git,不要继续对 .dockerignore 做无关修改。核对实际构建目标、镜像标签以及运行的镜像是否就是本次产物。
为最终阶段准备独立输出目录
通常可以让构建步骤只把交付文件放到一个输出目录,最终阶段只复制它。以下完整 Dockerfile 仅示意目录边界,不是实际应用构建脚本:
FROM alpine:3.22 AS builder
RUN mkdir -p /source/.git /output \
&& printf '%s\n' 'example page' > /output/index.html
FROM alpine:3.22
COPY --from=builder /output/ /app/
/source/.git 留在构建阶段,最终阶段只得到 /output/ 的内容。实际项目应将编译输出、必要运行资源与运行时依赖明确放到发布目录;不要照搬整个工作目录,再假定开发依赖和仓库历史会自动消失。
COPY --from 的源路径从所指定阶段的文件系统根解析。这里使用绝对路径,并不依赖源阶段最后一次设置的 WORKDIR。若输出为空或缺少运行资源,构建成功也不代表应用能够启动,应按项目产物清单验收。
确实需要保留大部分源码时,也可以在独立的发布准备阶段整理目录,或使用当前构建器支持的复制过滤能力。不要把 .dockerignore 按行交给 shell 执行删除:它的否定模式、注释与通配规则不是 shell 删除脚本,文件名还可能含空格。
COPY --exclude 有自己的版本条件
当前稳定 Dockerfile 语法从 1.19 支持 COPY --exclude。这是 Dockerfile 前端语法版本,不是 Docker Engine 的同号版本;旧构建器或旧语法声明可能不支持。若采用它,应明确所用前端和构建后端的兼容性,再按它的路径匹配规则验证实际目录树。
该选项并不等于自动加载 .dockerignore。需要排除哪些路径,仍须在复制规则中明确表达;有嵌套目录、同名文件或多个排除项时,使用项目真实目录样本核对结果。需要兼容旧构建流程时,独立输出目录通常更容易检查。
检查最终镜像与回退范围
修改前保留 Dockerfile、产物清单和旧镜像标识。新镜像应同时满足两个条件:不带本次明确排除的仓库数据,且应用必需的文件仍齐全。再通过项目已有测试确认启动、静态资源或运行任务正常。
如果私密信息已经进入旧镜像或构建缓存,新镜像不再复制它,不代表旧副本已经消失。应按实际暴露范围处理仓库、镜像仓库与凭据,不能只在最终阶段删除一个文件就宣称历史层安全。这里的目录整理也不能替代构建秘密的专用传递机制。
部署失败时恢复配套的旧 Dockerfile 与镜像引用。此次变更不需要删除运行数据卷;若旧镜像本身含有必须撤销的秘密,应先处理暴露风险,再决定是否允许回退到它。


