在 CI/CD 流水线里把构建产物打成镜像再推送,是现阶段容器化交付最省事、最不容易出错的做法先出产物,再碰镜像,谁的问题一眼就能看出来。
很多人第一次接触流水线时,习惯照着网上老教程一步步来:拉代码、装依赖、跑测试、构建、写 Dockerfile、再 docker build,这套流程跑得通,但到了真正维护的时候,痛点一个接一个,镜像里塞满了编译工具链,体积动不动上 GB,每次构建都像在重新发明一次轮子,把构建产物直接打成镜像,等于把“造轮子”和“装轮子”分开,流水线的每一段都只干一件事。
CI/CD 流水线镜像构建推送怎么做才最快
首先要明白,快并不单指构建速度快,而是整个交付链路顺畅,优化过几条流水线之后,我的体感是:流水线的瓶颈往往不在编译,而在镜像构建这一步重复拉取依赖和基础镜像,如果你还在流水线里用 docker build 现场编译,那每次代码变更,所有依赖层都面临失效的可能。
更稳的做法,是在流水线里先跑构建命令,产出 jar 包、静态文件或二进制,然后单独用一个精简的运行时基础镜像去打包,这个顺序调整,带来的变化是实实在在的:
- 构建环境和运行环境彻底解耦,编译用的 JDK、Node 版本随便换,不影响线上镜像
- 镜像体积通常能砍掉一大半,拉取和推送时间跟着降
- 流水线哪个环节挂了,日志里一眼就能定位是编译错还是打包错
具体到操作路径,拿 Java 项目举例,流水线里可以先跑 Maven 的 mvn clean package,产出 target 目录下的 jar 包,接着再写一个 Dockerfile,基础镜像直接指定 eclipse-temurin:17-jre-alpine,只把 jar 包 COPY 进去,这样整个镜像体积从三四百兆直接掉到一百多兆,推送时间肉眼可见地变短。
为什么直接打产物镜像能省下大量时间
关键在于少了一整轮环境准备,传统做法是流水线里用一个装好 Maven、JDK 和一堆依赖的构建镜像,每次跑流水线都相当于在容器里新开一个编译环境,依赖缓存能命中还好,一旦某个依赖版本更新,或者构建镜像被重新拉取,整个依赖树都要重新下载,跑一次流水线抽根烟都等不完。
直接把构建产物打成镜像,流水线后半段就变得非常轻,打包时你只需要关心文件 COPY 进去对不对,运行命令写没写对,其他一概不管,这种分工思路,行业共识认为正是云原生时代持续交付的核心用不可变产物去支撑部署,而不是每次用可变环境去碰运气。
流量入口加一层产物校验更稳妥
跳过产物直接构建镜像,最大的隐形风险是:你根本不确定打进镜像的代码是不是真的编译通过,有些流水线把测试和构建放一起,构建失败就不会推送,但测试只是跑通,产物不一定被干净地打进去,先产出产物,再加一道校验,不管是人工检查还是自动化脚本验证,都在推送前给交付质量上了双保险。
构建产物镜像和源码构建镜像,流水线里怎么选
这是一个被反复问到的问题,尤其是从传统部署转过来做容器化的团队。源码构建镜像,是把整个项目连同编译环境一起打进镜像里,运行容器时现场编译,产物构建镜像,是编译在流水线里完成,镜像里只装运行时和已经编译好的东西。
两者差别非常明显:
- 源码构建镜像胜在“一次构建到处跑”,环境自带,但镜像臃肿,安全漏洞面更大
- 产物构建镜像干净利落,可追溯性更强,但要求流水线必须能稳定产出可靠产物
多数团队走到后期都会切换到产物构建,拿一个上海的电商团队举例,他们做 Spring Boot 应用改造时,流水线原来是等代码拉下来直接 docker build,镜像里还带着完整的 JDK 和 Maven,体积接近 800MB,后来改成先在流水线里编译出 jar 包,再通过多阶段构建把 jar 包 COPY 进 JRE 基础镜像,镜像直接缩到 180MB 左右,同样的集群规模,镜像仓库存储成本省了一大截,部署拉镜像的时间缩短了将近一半。
Jenkins 流水线 Java 项目镜像推送实现步骤
以 Jenkins 为例,一条典型的产物镜像流水线可以拆成这么几段:
- 拉取代码,切换分支
- 执行构建命令,产出 jar 包,
mvn clean package -DskipTests - 执行测试,测试不过不往下走
- 用 Dockerfile 构建运行时镜像,基础镜像选 JRE 版本即可
- 给镜像打 tag,推送到私有仓库
这里有个容易踩坑的地方:Jenkins 默认的工作目录每次构建后可能被清理,jar 包如果没归档,下一步就找不到了,所以构建完产物后,要么用 archiveArtifacts 存起来,要么在同一个 workspace 里继续下一步,别换节点执行。
GitLab CI 的镜像推送还能再省一步
国内不少团队用 GitLab 做代码托管和 CI,GitLab CI 天然支持在流水线里直接构建和推送镜像,关键写法是定义 docker:dind 服务,然后在 job 里跑 docker 命令,产物构建模式在 GitLab CI 里体验更顺:第一个 job 负责编译,第二个 job 依赖第一个,直接在当前工作目录找到 jar 包,执行 docker build。
还可以通过 .gitlab-ci.yml 里的 only/except 规则控制,只让 main 分支的提交触发镜像推送,开发分支只跑到编译测试那一步,不给测试环境增加负担。
把构建产物打进镜像,这几点没处理好照样白搭
直接打产物镜像的思路没问题,但要是不注意细节,流水线依然会跑得磕磕绊绊,下面几个点,都是实操里容易忽略的。
产物目录别盲目 COPY
不要图省事直接 COPY . . 把整个构建上下文打进去,构建产物里经常混着测试报告、临时文件、甚至 .git 目录,全部塞进镜像不仅臃肿,还有泄露源码的风险,正确的姿势是精确到目录,Java 项目就 COPY target/app.jar /app/app.jar,前端项目就 COPY dist/ /usr/share/nginx/html/。
多阶段构建是产物镜像的最佳搭档
有时候你不想在流水线里单独跑一次编译,也可以把编译动作放进 Dockerfile 的第一阶段,第二阶段只 COPY 第一阶段产出的文件,这种方式也很适合产物镜像的思路:
- 第一阶段用 maven 镜像编译,生成 jar 包
- 第二阶段用 JRE 镜像,COPY 第一阶段产出的 jar 包
它的好处是 Jenkins 或 GitLab 里少配置一个编译步骤,但代价是 Dockerfile 里要维护两段环境,基础镜像更新时要同时关注两个阶段的版本。
镜像仓库的清理策略不能忘
流水线跑得频繁,镜像 tag 就容易堆积,每次推送都打 latest 的话,过不了几天仓库里就躺了几百个版本,存储成本蹭蹭涨,而且回滚时根本不知道哪个镜像对应哪个代码提交,多花一分钟,给镜像打上带有 commit 短 hash 的 tag,回滚时闭着眼睛都能找到目标。
镜像推送这块,产物直接构建到底适合什么场景
不是所有项目都适合直接走产物镜像这条路。如果你在做的是脚本类工具或者极轻量的 Python 应用,源码构建镜像反而更简单,因为本身就没什么编译过程,但只要是编译型语言,Java、Go、前端打包这一类,产出中间产物再接镜像,都是更可控的路径。
对于正在把旧项目改成容器部署的团队,我建议先别急着把整套流水线推倒重来,先挑一个业务逻辑简单的内部服务试试,把 Jenkins 或 GitLab 里的构建流程调整成先出产物再打镜像,跑顺一个再铺开,不少杭州的团队在做容器化改造时就是这么一步一步来的,反而比一口气全部迁移的成功率高,毕竟流水线这东西,适合自己业务的才是最好的。
CI/CD 流水线里构建产物镜像推送的常见问题解答
为什么构建产物打进镜像后运行时报缺少依赖?
最常见原因是基础镜像选得太精简,JRE 镜像不像 JDK 镜像那样自带完整工具链,一些项目在运行时依赖了外部命令,curl、fontconfig,精简镜像里没有,解决办法是使用带 -jre 标签但非 alpine 版本的镜像,或者直接在 Dockerfile 里用包管理器补装缺失的工具。
流水线里先构建产物再推镜像,和直接 Dockerfile 全打包相比哪个更推荐?
推荐先构建产物,全打包方式把编译工具链全部留在镜像里,镜像体积大,漏洞扫描报告里高危项层出不穷,先构建产物,镜像只保留运行时内容,维护成本和存储成本都更低,从安全角度也更符合最小化原则。
构建产物镜像推送失败,一般卡在哪一步?
九成情况出在权限配置上,CI 环境执行 docker push 需要登录凭证,凭证过期或者只配置了拉取权限,推送自然失败,另外就是镜像 tag 命名不规范,包含特殊字符,或者仓库地址写错,处理方式是优先检查 CI 变量里的仓库凭证,然后确认 tag 符合仓库命名规范。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/639529.html





