28、Gradle与Docker:使用Gradle构建Docker镜像、Docker插件、多阶段构建集成

说实话,Docker 和 Gradle 的搭配,是我这几年做 CI/CD 时最常用的组合之一。以前我们团队都是手动写 Dockerfile,然后跑 docker build,再 push 到仓库。后来项目多了,发现每次都要手动维护版本号、标签、构建参数,特别容易出错。嗯,这时候 Gradle 的 Docker 插件就派上用场了。

我个人习惯把镜像构建也纳入 Gradle 的生命周期管理里。这样你只需要执行一个 ./gradlew buildImage,就能完成编译、打包、镜像构建、推送一条龙。说白了,就是把 Docker 当成 Gradle 的一个 Task 来调度。

28.1 为什么要在 Gradle 里集成 Docker?

你想想看,一个典型的 Java 微服务项目,从源码到 Docker 镜像,中间要经历多少步?编译、测试、打包、构建镜像、打标签、推送到仓库。如果每一步都手动操作,不仅效率低,还容易漏掉某个环节。

我在项目中遇到过最坑的一次:开发同学改了代码,但忘了更新 Dockerfile 里的 JAR 包版本号,结果线上跑的还是旧代码。从那以后,我就坚持把镜像构建自动化到 Gradle 里,让版本号从构建产物里自动获取,彻底杜绝人为失误。

核心价值:将 Docker 构建纳入 Gradle 的依赖管理和增量构建体系,实现「一次构建,处处运行」的自动化流水线。

28.2 主流 Docker 插件对比

Gradle 生态里 Docker 插件不少,我用过的有三个比较主流的。下面这张表帮你快速对比一下:

插件名称 维护方 特点 适用场景
com.bmuschko.docker-remote-api bmuschko 纯 Gradle API 调用,不依赖 Docker CLI CI/CD 环境,无 Docker 守护进程
com.bmuschko.docker-spring-boot bmuschko 专为 Spring Boot 优化,自动处理分层镜像 Spring Boot 项目
com.avast.gradle.docker-compose Avast 集成 Docker Compose,适合多容器测试 集成测试环境

我个人最常用的是 com.bmuschko.docker-remote-api。为什么?因为它不依赖本机安装的 Docker CLI,直接通过 Docker 的 REST API 通信。这在 Jenkins 等 CI 环境里特别香——你不需要在构建节点上装 Docker 客户端,只要配置好 Docker 守护进程的地址就行。

28.3 快速上手:配置 Docker 插件

咱们直接看代码。先在你的 build.gradle 里引入插件:

plugins {
    id 'java'
    id 'com.bmuschko.docker-remote-api' version '9.3.1'
}

import com.bmuschko.gradle.docker.tasks.image.*
import com.bmuschko.gradle.docker.tasks.container.*

// 配置 Docker 客户端连接信息
docker {
    url = 'unix:///var/run/docker.sock'
    // 如果是远程 Docker,可以这样配:
    // url = 'tcp://192.168.1.100:2375'
    // certPath = file('/path/to/certs')
}

嗯,这里要注意:如果你用的是远程 Docker,一定要开启 TLS 认证,否则有安全风险。我曾经见过有人直接把 Docker 端口暴露到公网,结果被挖矿程序盯上了……

28.4 构建 Docker 镜像的典型流程

我习惯把镜像构建拆成三个 Task:

  1. 准备构建上下文:把 JAR 包、配置文件复制到临时目录
  2. 构建镜像:执行 docker build
  3. 打标签并推送:给镜像打上版本号和 latest 标签,然后 push

下面是一个完整的示例:

task prepareDockerContext(type: Sync) {
    dependsOn bootJar
    from file('src/main/docker')
    from bootJar.outputs.files
    into file("${buildDir}/docker")
}

task buildDockerImage(type: DockerBuildImage) {
    dependsOn prepareDockerContext
    inputDir = file("${buildDir}/docker")
    images.add("myapp:${project.version}")
    images.add("myapp:latest")
}

task pushDockerImage(type: DockerPushImage) {
    dependsOn buildDockerImage
    images = buildDockerImage.images
}

你看,这里用到了 Gradle 的 Sync 任务类型。它比 Copy 更智能——如果源文件没变化,它不会重复执行。这就是增量构建的威力。

避坑指南:我曾经犯过一个错误,把整个 build 目录都复制到 Docker 上下文里,结果镜像体积飙到 2GB。后来只复制 JAR 包和必要的配置文件,镜像直接降到 200MB。记住:Docker 上下文越小,构建速度越快。

28.5 多阶段构建集成

多阶段构建是 Docker 17.05 引入的特性。它的核心思想是:用一个基础镜像来编译代码,再用另一个更小的镜像来运行产物。这样最终镜像里只包含运行时依赖,不包含编译工具链。

在 Gradle 里集成多阶段构建,我通常的做法是:

  1. 第一阶段:用 Gradle 的 java 插件编译代码,生成 JAR 包
  2. 第二阶段:在 Dockerfile 里用 FROM openjdk:11-jre-slim 作为运行镜像
  3. 把第一阶段的 JAR 包复制到第二阶段

下面是一个典型的 Dockerfile 示例:

# 第一阶段:构建阶段
FROM gradle:7.4-jdk11 AS builder
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle build --no-daemon

# 第二阶段:运行阶段
FROM openjdk:11-jre-slim
COPY --from=builder /home/gradle/src/build/libs/*.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

但这里有个问题:每次构建都要重新下载 Gradle 依赖,很慢。我的优化方案是:在 Gradle 里先执行 build,然后把 JAR 包传给 Dockerfile,让 Dockerfile 只做「复制+运行」的工作。

task buildWithDockerMultiStage(type: DockerBuildImage) {
    dependsOn bootJar
    inputDir = file("${buildDir}/docker")
    dockerFile = file("${buildDir}/docker/Dockerfile.multistage")
    images.add("myapp:${project.version}")
}

对应的 Dockerfile 就简化成:

FROM openjdk:11-jre-slim
COPY *.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

这样 Gradle 负责编译和依赖管理,Docker 只负责打包运行环境。各司其职,构建速度提升不少。

28.6 增量构建与缓存优化

Docker 构建本身有层缓存机制。但如果你在 Gradle 里每次都重新生成 Dockerfile,缓存就失效了。我的经验是:

  • 把不变的依赖层(如基础镜像、系统包)放在 Dockerfile 前面
  • 把经常变化的代码层(如 JAR 包)放在后面
  • 利用 Gradle 的 upToDateWhen 条件,避免重复构建
buildDockerImage {
    // 只有当 JAR 包变化时才重新构建镜像
    inputs.files bootJar.outputs.files
    outputs.upToDateWhen {
        !inputs.files.files.any { it.lastModified() > outputDir.lastModified() }
    }
}

嗯,这段代码的意思是:如果 JAR 包的最后修改时间比镜像的输出目录还早,说明镜像已经是最新的,跳过构建。这个技巧在 CI 流水线里能省下不少时间。

28.7 实战:完整的 Docker 构建脚本

最后,我分享一个生产环境里用到的完整脚本。它包含了版本管理、镜像标签、推送、清理等环节:

def dockerTag = project.hasProperty('dockerTag') ? project.dockerTag : project.version

task dockerBuild(type: DockerBuildImage) {
    dependsOn bootJar
    inputDir = file("${buildDir}/docker")
    images.add("registry.example.com/myapp:${dockerTag}")
    images.add("registry.example.com/myapp:latest")
}

task dockerPush(type: DockerPushImage) {
    dependsOn dockerBuild
    images = dockerBuild.images
}

task dockerClean(type: DockerRemoveImage) {
    force = true
    images = ["registry.example.com/myapp:${dockerTag}"]
}

使用方式很简单:

# 构建并推送
./gradlew dockerPush -PdockerTag=v1.2.3

# 清理本地镜像
./gradlew dockerClean
注意:推送镜像前,记得先执行 docker login 或者配置 Docker 认证。否则会报权限错误。我建议在 Gradle 脚本里不要硬编码密码,而是通过环境变量或 CI 的 Secret 管理工具传入。

28.8 本章小结

Gradle 与 Docker 的集成,说白了就是让构建工具接管容器化流程。你不需要再手动写 docker build 命令,也不用担心版本号对不上。通过插件配置、多阶段构建、增量缓存优化,你可以把整个流程自动化、标准化。

我个人觉得,这套方案最大的价值在于「可复现」。不管是谁、在哪个环境执行 ./gradlew dockerPush,得到的镜像都是一模一样的。这才是 DevOps 追求的「不可变基础设施」。

一句话总结:把 Docker 当成 Gradle 的一个 Task,让构建工具替你管理镜像的生命周期。
Gradle + Docker 集成流程 Gradle 编译 ./gradlew bootJar 准备 Docker 上下文 Sync 任务复制 JAR 构建 Docker 镜像 DockerBuildImage 多阶段构建优化 第一阶段:Gradle 编译 + 依赖下载(使用 gradle:7.4-jdk11 镜像) 第二阶段:仅复制 JAR 到 openjdk:11-jre-slim 运行镜像 优势:最终镜像体积减少 80%,不包含编译工具链 增量构建缓存:仅当 JAR 文件变化时才重新构建镜像 upToDateWhen 条件判断 + Docker 层缓存 推送镜像到仓库:dockerPush
公众号:蓝海资料掘金营,微信 deep3321