如何为 Java 应用程序构建 docker 容器
Posted
技术标签:
【中文标题】如何为 Java 应用程序构建 docker 容器【英文标题】:How to build a docker container for a Java application 【发布时间】:2015-10-20 04:44:07 【问题描述】:我想做的是为我的 Java 应用程序构建一个 docker 映像,但以下注意事项对于大多数编译语言来说应该是正确的。
问题
在我的构建服务器上,我想为我的应用程序生成一个 docker 映像作为可交付成果。为此,我必须使用一些构建工具(通常是 Gradle、Maven 或 Ant)编译应用程序,然后将创建的 JAR 文件添加到 docker 映像中。因为我希望 docker 镜像只执行 JAR 文件,所以我当然会从已安装 Java 的基础镜像开始。
有以下三种方法:
让构建工具控制进程
在这种情况下,我的构建工具控制着整个过程。因此它准备了 JAR 文件,并在创建 JAR 后调用 Docker 来创建映像。这是因为 JAR 是预先创建的,并且 Docker 可以忽略创建 JAR 所需的构建过程。
但我的 Dockerfile 不再是独立的。这取决于在 Docker 之外发生的步骤才能正常工作。在我的 Dockerfile 中,我将有一个 COPY
或 ADD
语句应该将 JAR 文件复制到映像。如果没有事先创建 jar,此语句将失败。所以仅仅执行 Dockerfile 可能行不通。如果您想与仅使用当前 Dockerfile 构建的服务(例如 DockerHub 上的自动构建功能)集成,这将成为一个问题。
让 Docker 控制构建
在这种情况下,创建镜像的所有必要步骤都添加到 Dockerfile 中,因此只需执行 Docker 构建即可创建镜像。
这种方法的主要问题是无法将应该在正在创建的 docker 映像之外执行的命令添加到 Dockerfile 中。这意味着我必须将我的源代码和我的构建工具添加到 docker 镜像中,并在镜像中构建我的 JAR 文件。这将导致我的图像比它必须的更大,因为添加的所有文件在运行时都是不必要的。这也会为我的图像添加额外的图层。
编辑:
正如@adrian-mouat 指出的那样,如果我在一个 RUN 语句中添加源、构建应用程序并删除源,我可以避免向 Docker 映像添加不必要的文件和层。这意味着创建一些疯狂的链式命令。
两个独立的构建
在这种情况下,我们将构建分为两部分:首先,我们使用构建工具创建 JAR 文件并将其上传到存储库(Maven 或 Ivy 存储库)。然后我们触发一个单独的 Docker 构建,它只是从存储库中添加 JAR 文件。
结论
在我看来,更好的方法是让构建工具控制过程。这将产生一个干净的 docker 镜像,因为镜像是我们想要交付的,所以这很重要。为避免出现可能无法正常工作的 Dockerfile,应将其创建为构建的一部分。所以没有人会不小心使用它来启动一个损坏的构建。
但这不允许我与 DockerHub 集成。
问题
还有其他我想念的方法吗?
2020 年 6 月更新
自从我第一次提出这个问题以来的几年里,很多东西都发生了变化。在这一点上,我会提倡使用Googel's JIB Tool。它与最常见的 Java 构建工具(Maven 和 Gradle)集成,并允许您直接从构建中创建容器。这比我多年前考虑的任何旧方法都简洁得多。
2021 年 2 月更新
我发现 James Ward 的这篇博文和视频更好地反映了当前最先进的技术。 https://cloud.google.com/blog/topics/developers-practitioners/comparing-containerization-methods-buildpacks-jib-and-dockerfile
【问题讨论】:
既然已经有了可以自动化的构建工具,为什么还需要与 DockerHub 集成? @Thomasleveil 因为我不想运行单独的 CI 服务器来构建 docker 映像并将其推送到 DockerHub 的注册表中,因为我可以从 DockerHub 免费获得它。能够将 DockerHub 上的自动构建功能与 Java 应用程序一起使用会很棒,但我不知道如何最好地做到这一点。 如果你真的想从免费的 DockerHub 自动化服务中获利,那么看起来你必须做出妥协,让 docker 进行 Ant/Maven/Gradle 构建 关于“这意味着创建一些疯狂的链式命令”,不一定,你可以将这些命令放在一个shell脚本中,COPY
将shell脚本放入你的图像中,然后使用RUN
指令调用脚本→生成一层
您能否澄清一下您是想使用自己的构建服务器(“在我的构建服务器上,我想生成一个 docker 映像”)还是使用“Docker 自动构建功能”作为在上面的 cmets 中提到(因此不包括自己的基础设施)?
【参考方案1】:
docker 注册中心有一个Maven image,可用于创建 java 容器。
使用这种方法,构建机器不需要预先安装 Java 或 Maven,Docker 可以控制整个构建过程。
示例
├── Dockerfile
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── org
│ │ └── demo
│ │ └── App.java
│ └── resources
│ └── log4j.properties
└── test
└── java
└── org
└── demo
└── AppTest.java
图像构建如下:
docker build -t my-maven .
并运行如下:
$ docker run -it --rm my-maven
0 [main] INFO org.demo.App - hello world
Dockerfile
FROM maven:3.3-jdk-8-onbuild
CMD ["java","-jar","/usr/src/app/target/demo-1.0-SNAPSHOT-jar-with-dependencies.jar"]
更新
如果你想优化你的镜像以排除源,你可以创建一个只包含构建的 jar 的 Dockerfile:
FROM java:8
ADD target/demo-1.0-SNAPSHOT-jar-with-dependencies.jar /opt/demo/demo-1.0-SNAPSHOT-jar-with-dependencies.jar
CMD ["java","-jar","/opt/demo/demo-1.0-SNAPSHOT-jar-with-dependencies.jar"]
并分两步构建镜像:
docker run -it --rm -w /opt/maven \
-v $PWD:/opt/maven \
-v $HOME/.m2:/root/.m2 \
maven:3.3-jdk-8 \
mvn clean install
docker build -t my-app .
__
更新 (2017-07-27)
Docker 现在具有multi-stage build 功能。这使 Docker 能够构建包含构建工具但仅包含运行时依赖项的映像。
下面的例子演示了这个概念,注意jar是如何从第一个构建阶段的目标目录复制的
FROM maven:3.3-jdk-8-onbuild
FROM java:8
COPY --from=0 /usr/src/app/target/demo-1.0-SNAPSHOT.jar /opt/demo.jar
CMD ["java","-jar","/opt/demo.jar"]
【讨论】:
@Stephane 第一个示例使用包含 Java 和 Maven 的基础映像。源代码构建在容器内。第二个示例是为了回应 Tobias 的评论,即他希望源不在容器内,需要两步过程,首先构建 jar,然后创建一个包含 Java 基础映像和构建的 jar 的容器。 @DarioBB 用于构建“maven:3.3-jdk-8-onbuild”映像的 Docker 文件将工作目录设置为 /usr/src/app 并将本地文件复制到此位置。请参阅:github.com/carlossg/docker-maven/blob/…。我是如何发现这个 Docker 文件的?从 DockerHub 链接:hub.docker.com/_/maven @DarioBB 再次查看 onbuild 映像的 Dockerfile。它将工作目录设置为“/usr/src/app”。其次,它提供了两个 ONBUILD 指令。第一个将您的源代码复制到“/usr/src/app”,第二个将运行“mvn install”。那是什么意思?您的 jar 文件将在“/usr/src/app/target”目录中创建 让我们continue this discussion in chat。 请注意,Maven 不是为共享~/.m2/repo
而设计的。您应该只在确定一次只运行一个实例的情况下执行此操作。【参考方案2】:
java应用的结构
Demo
└── src
| ├── main
| │ ├── java
| │ │ └── org
| │ │ └── demo
| │ │ └── App.java
| │ └── resources
| │ └── application.properties
| └── test
| └── java
| └── org
| └── demo
| └── App.java
├──── Dockerfile
├──── pom.xml
Dockerfile 的内容
FROM java:8
EXPOSE 8080
ADD /target/demo.jar demo.jar
ENTRYPOINT ["java","-jar","demo.jar"]
构建和运行映像的命令
进入项目目录,比如说D:/Demo
$ cd D/demo
$ mvn clean install
$ docker build demo .
$ docker run -p 8080:8080 -t demo
检查容器是否正在运行
$ docker ps
输出将是
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
55c11a464f5a demo1 "java -jar demo.jar" 21 seconds ago Up About a minute 0.0.0.0:8080->8080/tcp cranky_mayer
【讨论】:
【参考方案3】:最简单的方法是让构建工具控制流程。否则,您将不得不维护构建工具的构建文件(如 Maven 的 pom.xml
或 Gradle 的 build.gradle
)以及 Dockerfile
。
为您的 Java 应用程序构建 Docker 容器的一种简单方法是使用 Jib,它可以作为 Maven 和 Gradle 插件使用。
例如,如果您使用 Maven 并希望将容器构建到正在运行的 Docker 守护程序,则只需运行以下命令:
mvn compile com.google.cloud.tools:jib-maven-plugin:0.9.2:dockerBuild
您也可以使用 Jib build directly to a Docker registry,而无需安装 docker
、运行 Docker 守护程序(需要 root 权限)或编写 Dockerfile
。它也更快并且可重现地构建图像。
在 Github 存储库中查看有关 Jib 的更多信息:https://github.com/GoogleContainerTools/jib
【讨论】:
你救了我的命,你给出的命令比文档中的命令要好。【参考方案4】:我们用了一段时间Spotify Docker Maven Plugin。该插件允许您将 Docker 构建绑定到 Maven 生命周期的某个阶段。
一个例子: 通过配置插件将构建的应用程序作为资源添加到 Docker 构建上下文中,在打包(阶段:打包)您的应用程序后运行 Docker 构建。在部署阶段运行 Docker 推送目标以将您的 Docker 映像推送到注册表。这可以在普通部署插件旁边运行,后者将工件发布到 Nexus 等存储库中。
后来,我们在 CI 服务器上将构建拆分为两个单独的作业。由于 Docker 只是运行应用程序的一种方式(有时我们需要在不同环境中发布应用程序而不仅仅是 Docker),因此 Maven 构建不应依赖 Docker。
所以第一个作业在 Nexus 中发布应用程序(通过 Maven 部署)。第二个作业(可以是第一个作业的下游依赖项)下载最新发布的工件,执行 Docker 构建并将映像推送到注册表。为了下载最新版本,我们使用Versions Maven Plugin (versions:use-latest-releases) 以及Maven Dependency Plugin (dependency:get 和dependency:copy)。
也可以为特定版本的应用程序启动第二个作业,以(重新)为旧版本构建 Docker 映像。此外,您可以使用构建管道(在 Jenkins 上),它执行这两个作业并将发布版本或发布工件传递给 Docker 构建。
【讨论】:
【参考方案5】:使用 Jib 工具将您的 Java 应用程序容器化,而无需编写 dockerfile
Jib 是由 Google 维护的开源 Java 工具,用于构建 Java 应用程序的 Docker 映像。它简化了容器化,因为有了它,我们不需要编写 dockerfile。实际上,我们甚至不必安装 docker 就可以自己创建和发布 docker 映像。
Google 将 Jib 发布为 Maven 和 Gradle 插件。 https://github.com/GoogleContainerTools/jib
使用 Maven 项目将您的 java 应用程序容器化
https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#quickstart
使用 Gradle 项目容器化您的 Java 应用程序
https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin#quickstart
【讨论】:
我更喜欢吊臂。正如 baeldung 所说:“还有一些其他工具,比如 Spotify 的 docker-maven-plugin 和 dockerfile-maven 插件,尽管前者现在已被弃用,而后者需要 dockerfile。”【参考方案6】:有几点:
如果您在添加文件的同一条指令中删除文件,它们将不会占用映像中的空间。如果您查看官方图像的一些 Dockerfile,您会看到它们下载源代码、构建它并在同一步骤中将其全部删除(例如 https://github.com/docker-library/python/blob/0fa3202789648132971160f686f5a37595108f44/3.5/slim/Dockerfile)。这确实意味着你需要做一些烦人的体操,但这是完全可行的。
我没有看到两个单独的 Dockerfile 的问题。这样做的好处是您可以使用 JRE 而不是 JDK 来托管您的 jar。
【讨论】:
【参考方案7】:运行 jar 或 war 包还有其他用途
将 jar 添加到图像中。 为 java 设置堆大小 通过入口点运行 jar 命令示例 dockerfile
FROM base
ADD sample.jar renamed.jar
ENV HEAP_SIZE 256m
ENTRYPOINT exec java -Xms$HEAP_SIZE -Xmx$HEAP_SIZE -jar renamed.jar
另外在tomcat上的包部署示例
FROM tomcat7
ADD sample.war $CATALINA_HOME/webapps/ROOT.war
CMD $CATALINA_HOME/bin/catalina.sh run
将 dockerfiles 构建为图像
cp tomcat.dockerfile /workingdir/Dockerfile
docker build -t name /workingdir/Dockerfile .
列出图片
docker images
使用图片创建容器
docker run --name cont_name --extra-vars var1=val1 var2=val2 imagename
【讨论】:
【参考方案8】:Here 我描述了我是如何在我的开发环境中做到这一点的。
使用 Maven 在本地构建 war/jar 将其复制到本地 Docker 文件夹 运行Intellij Docker plugin创建一个包含war/jar的docker镜像,运行应用服务器并将其部署到远程Docker服务器上希望对你有帮助。
【讨论】:
以上是关于如何为 Java 应用程序构建 docker 容器的主要内容,如果未能解决你的问题,请参考以下文章
如何为包含 kafka、postgres 和 rest api docker 容器的应用程序编写 e2e 测试自动化