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 里,让版本号从构建产物里自动获取,彻底杜绝人为失误。
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:
- 准备构建上下文:把 JAR 包、配置文件复制到临时目录
- 构建镜像:执行 docker build
- 打标签并推送:给镜像打上版本号和 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 更智能——如果源文件没变化,它不会重复执行。这就是增量构建的威力。
28.5 多阶段构建集成
多阶段构建是 Docker 17.05 引入的特性。它的核心思想是:用一个基础镜像来编译代码,再用另一个更小的镜像来运行产物。这样最终镜像里只包含运行时依赖,不包含编译工具链。
在 Gradle 里集成多阶段构建,我通常的做法是:
- 第一阶段:用 Gradle 的
java插件编译代码,生成 JAR 包 - 第二阶段:在 Dockerfile 里用
FROM openjdk:11-jre-slim作为运行镜像 - 把第一阶段的 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 追求的「不可变基础设施」。