图片 3

用容器重新定义 Java 虚拟化部署实战案例

【编者的话】这是一篇入门级的学习教程,推荐Java开发者阅读,作者通过一个简单的例子演示了如何在Docker中进行Java开发。不需要Maven、不需要JDK,你只需要给你的小伙伴一个Dockerfile,剩下的事情交给Docker去完成吧。

图片 1

两天前小希和大家分享了《用容器定义 Java
虚拟化部署》,估计有些小伙伴早已按耐不住着急的心情了吧。今天希云就和大家分享在容器里部署
Java 应用的实战案例。

这周,我和Anna、Stephan、Timo在慕尼黑的W-Jax开了一个关于企业技术(特别针对Java)的会议。没想到居然有这么多的人对Docker感兴趣,但问题是怎么在Docker上进行Java开发呢?我个人比较喜欢短小的示例,它可以通过包含几个小文件的框架帮助你了解某个技术。不幸的是,这在Java的世界很难实现,因为大多数的示例都需要某个IDE以及适当的对Web框架有所了解。在这篇文章中,我将尝试使用短小的示例,以帮助你快速学习如何在Docker中进行Java开发。

Java 工程师如何在 Docker
上进行开发?本文能让你以最小的日常开支和预备知识就可以把 Docker 和 Java
结合使用。

Dockerfiles

准备工作

现在有非常多的Java
Web框架,但我这里并不打算使用它们。我只想要的是一个小的框架所以我选择了Spark,它是一个基于Java
8的极小的框架。Spark使用Maven作为构建工具。

图片 2

安装

Dockerfile
包含了一系列指令,告诉容器如何去构建一个镜像,它指定了镜像的基点,以及配置镜像的每个细节。以下是一个
Dockerfile 示例,是 CentOS 镜像的 Dockerfile 。

源代码和配置文件

在这个例子中你要增加三个文件:

  • Maven的配置文件: pom.xml
  • 一个Java类:Hello.java
  • 一个Dockerfile

如果有读者等不及了,可以克隆这个repo:

下面我们会详细解释这三个文件的结构,你可以此视频来快速了解。(读者可以查看原文中的视频,看完视频基本可以了解怎么做)

现在有很多的 Java Web 框架,挑选一个非常小的框架,选择 Spark
吧!它是一款基于 Java-8 的微型 Sinatra 框架。如果你去阅读 Spark
的说明文档,会了解它是用 Maven 作为其构建工具。

代码清单 1. CentOS Dockerfile

pom.xml

pom.xml包含一些基本的Maven配置,比如配置Spark所依赖的Java
8。它会把所有的依赖封装成一个大的jar包。我不是
Maven专家,所以我没法把例子写得更简单、更流畅以便让他们更受欢迎。这是pom文件地址,你可以看看我的配置:https://gist.github.com/luebke

m-xml

在本示例里,会利用 Maven 和 Docker 的分层文件系统( UnionFS
),从零开始安装一切。与此同时,当重新编译变动的内容时,也需要一些时间。

sh FROM scratch

Hello.java

pom.xml文件定义mainClass为sparkexample.Hello,我们需要在src/main/java/sparkexample/目录下创建Hello.java文件。

因此,你需要的预备知识是:无需 Java,无需 Maven,只需 Docker。

MAINTAINER The CentOS Project <cloud-ops@centos.org> –
ami_creator

Dockerfile

最后我们来编写Dockerfile文件,这个Dockerfile使用到了Java镜像(java:oracle-java8),并从安装Maven开始做起。下一步它会安装项目依赖。我们通过pom.xml来解析这些依赖,正如你所看到的,它允许Docker缓存这些依赖。下一步,我们要编译打包我们的应用,并启动应用。如果我们重建应用时,pom.xml文件没有任何修改,之前的步骤都被缓存下来了,直接到最后一步启动应用。这可以加快应用的重新构建速度。

源代码和配置文件

ADD centos-7-20150616_1752-docker.tar.xz /

创建和运行

一旦这三个文件已经完成,那创建Docker镜像就变得轻而易举了。

$ docker build -t giantswarm/sparkexample

注意:首次启动时会花费一些时间,因为它要安装Maven并下载所有的依赖。之后再启动就需要几秒钟,因为所有的东西都已经缓存了。
镜像创建之后,用下面的命令创建容器:

docker run -d -p 4567:4567 giantswarm/sparkexample

用下面的命令访问:

curl localhost:4567 hello from sparkjava.com

现在可以去修改源码(返回你想返回的东西)并重新构建,这看起来是不是很棒?

本例中,你必须得添加以下3个文件:

Volumes for systemd

Maven配置: pom.xml

VOLUME [“/run”, “/tmp”]

Java文件: Hello.java

Environment for systemd

Dockerfile

ENV container=docker

如果觉得篇幅过长,可以直接克隆如下repo

For systemd usage this changes to /usr/sbin/init

git clone giantswarm/sparkexample pom.xml

Keeping it as /bin/bash for compatibility with previous

pom.xml

CMD [“/bin/bash”]

pom.xml 文件包含一个基本的 Maven 配置。这个大家都很熟悉的了!它用
Java1.8 编译器配置 Spark 的依赖项,并用所有依赖项创建一个大的 jar
包。有多大啊?不过肯定要比希云的微镜像大!

大部分内容是注释,主要有四句命令:

Hello.java

pom.xml文件定义了一个类名叫:sparkexample.Hello 的 main
class(主类)。在子路径 src/main/java/sparkexample/ 下创建 Hello.java
文件。

FROM scratch:所有 Dockerfile
都要从一个基础镜像继承,在这个例子中,CentOS 镜像是继承于” scratch
“镜像,这个镜像是所有镜像的根。这个配置是固定的,表明了这个是容器的根镜像之一。

正如你看到的,这是最新的 Java 代码:静态导入和 lambda
表达式,使该例子非常紧凑。类包含一个 main method(主要方法),响应 root
请求 (“/“)。像 HelloWorld 一样普通,响应只是简单的字符串。

MAINTAINER ...MAINTAINER指令指明了镜像的所有者,这个例子中所有者是
CentOS Project。

Dockerfile

ADD centos...tar.xzADD指令告诉容器把指定文件上传到镜像中,如果文件是压缩过的,会把它解压到指定路径。这个例子中,容器会上传一个
CentOS 操作系统的 Gzip 包,并解压到系统的根目录。

最后一个也是,最重要的一个文件: Dockerfile

CMD ["/bin/bash"]:最后,CMD指令告诉容器要执行什么命令,这个例子中,最后会进入
Bourne Again Shell (bash) 终端。

FROM java:8Install mavenRUN apt-get update RUN apt-get install -y maven
WORKDIR /code Prepare by downloading dependenciesADD pom.xml
/code/pom.xml RUN [“mvn”, “dependency:resolve”] RUN [“mvn”,
“verify”] Adding source, compile and package into a fat jarADD src
/code/src RUN [“mvn”, “package”] EXPOSE 4567 CMD
[“/usr/lib/jvm/java-8-openjdk-amd64/bin/java”, “-jar”,
“target/sparkexample-jar-with-dependencies.jar”]

图片 3

基于 java8,安装 Maven ,和构建 jar 包。(如想构建 jdk,jre 的 docker
镜像请点这里)。通过添加
pom.xml 文件解析依赖项实现构建。

这个架构未必如你想象中那么简单,但我们接下来会慢慢学习它,其实它是非常有逻辑的。上边已经提过所有
Dockerfile 的根是 scratch,接下来指定的是 debian:jessie
镜像,这个官方镜像是基于标准镜像构建的,容器不需要重复发明轮子,每次都创建一个新镜像了,只要基于一个稳定的镜像来继续构建新镜像即可,在这个例子中,
debian:jessie 是一个官方 Debian Linux 镜像,就像上边的 CentOS
一样,它只有三行指令。

实际操作过程中会发现,如果我们不改变 pom.xml 而想重新编译
app,之前的步骤已被缓存,只运行最后的步骤,这将使重编译速度更快。这点
docker 的优势非常明显!

代码清单 2. debian:jessie Dockerfile

创建和运行

sh FROM scratch

有以上 3 个文件后,创建 Docker 镜像就变得非常简单:

ADD rootfs.tar.xz /

$ docker build -t csphere/sparkexample .

CMD [“/bin/bash”]

注意:第一次构建时,需要等一会。需要先下载安装
Maven,还会下载所有项目依赖项。以后每次编译启动只需要几秒钟就可以了,不要问我为什么为什么只需几秒,因为有缓存。

在上图中我们还见到有安装两个额外的镜像,CURL 和 Source Code
Management,镜像buildpack-deps:jessie-curl的Dockerfile 如清单 3 所示。

镜像创建好后,启动容器:

代码清单 3. buildpack-deps:jessie-curl Dockerfile

$ docker run -d -p 4567:4567 csphere/sparkexample

sh FROM debian:jessie

测试:

RUN apt-get update && apt-get install -y –no-install-recommends

$ curl localhost:4567

ca-certificates

hello
fromhttp://sparkjava.com

curl

创建镜像就这么简单!现在,可以去修改源代码,再重新编译,是不是很简单,很棒呢?!

wget

结论

&& rm -rf /var/lib/apt/lists/*

虽然这只是个基础的例子,但是我们仍然希望,你敢于尝试并且热衷于在 Docker
上进行

这个 Dockerfile 中使用 apt-get 去安装 curl
wget,使这个镜像能从其他服务器下载软件。RUN
指令让Docker在运行的实例中执行具体的命令,这个例子中,它会更新所有库
(apt-get update),然后执行 apt-get install 去安装 curlwget

buildpack-deps:jessie-scp 的 Dockerfile 如清单 4 所示。

代码清单 4. buildpack-deps:jessie-scp Dockerfile

sh FROM buildpack-deps:jessie-curl

RUN apt-get update && apt-get install -y –no-install-recommends

bzr

git

mercurial

openssh-client

subversion

&& rm -rf /var/lib/apt/lists/*

这个Dockerfile会安装源码管理工具,例如
Git,Mercurial,和 Subversion。

Java
的Dockerfile会更加复杂些,如清单
5 所示。

代码清单 5. Java Dockerfile

sh FROM buildpack-deps:jessie-scm

A few problems with compiling Java from source:

  1. Oracle. Licensing prevents us from redistributing the official JDK.

  2. Compiling OpenJDK also requires the JDK to be installed, and it gets

really hairy.

RUN apt-get update && apt-get install -y unzip && rm -rf
/var/lib/apt/lists/*

RUN echo ‘deb jessie-backports main’
> /etc/apt/sources.list.d/jessie-backports.list

Default to UTF-8 file.encoding

ENV LANG C.UTF-8

ENV JAVA_VERSION 8u66

ENV JAVA_DEBIAN_VERSION 8u66-b01-1~bpo8+1

see

and … 46872

ENV CA_CERTIFICATES_JAVA_VERSION 20140324

RUN set -x

&& apt-get update

&& apt-get install -y

openjdk-8-jdk=”$JAVA_DEBIAN_VERSION”

ca-certificates-java=”$CA_CERTIFICATES_JAVA_VERSION”

&& rm -rf /var/lib/apt/lists/*

see CA_CERTIFICATES_JAVA_VERSION notes above

RUN /var/lib/dpkg/info/ca-certificates-java.postinst configure

If you’re reading this and have any feedback on how this image could be

improved, please open an issue or a pull request so we can discuss it!

简单来说,这个 Dockerfile 使用了安全参数去执行
apt-get install -y openjdk-8-jdk 去下载安装 Java,而 ENV
指令配置系统的环境变量。

最后,清单 6 是 Tomcat
的Dockerfile

代码清单 6. Tomcat Dockerfile

sh FROM java:7-jre

ENV CATALINA_HOME /usr/local/tomcat

ENV PATH $CATALINA_HOME/bin:$PATH

RUN mkdir -p “$CATALINA_HOME”

WORKDIR $CATALINA_HOME

see

RUN gpg –keyserver pool.sks-keyservers.net –recv-keys

05AB33110949707C93A279E3D3EFE6B686867BA6

07E48665A34DCAFAE522E5E6266191C37C037D42

47309207D818FFD8DCD3F83F1931D684307A10A5

541FBE7D8F78B25E055DDEE13C370389288584E7

61B832AC2F1C5A90F0F9B00A1C506407564C17A3

79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED

9BA44C2621385CB966EBA586F72C284D731FABEE

A27677289986DB50844682F8ACB77FC2E86E29AC

A9C5DF4D22E99998D9875A5110C01C5A2F6059E7

DCFD35E0BF8CA7344752DE8B6FB21E8933C60243

F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE

F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23

ENV TOMCAT_MAJOR 8

ENV TOMCAT_VERSION 8.0.26

ENV TOMCAT_TGZ_URL

RUN set -x

&& curl -fSL “$TOMCAT_TGZ_URL” -o tomcat.tar.gz

&& curl -fSL “$TOMCAT_TGZ_URL.asc” -o tomcat.tar.gz.asc

&& gpg –verify tomcat.tar.gz.asc

&& tar -xvf tomcat.tar.gz –strip-components=1

&& rm bin/*.bat

&& rm tomcat.tar.gz*

EXPOSE 8080

CMD [“catalina.sh”, “run”]

严格来说,Tomcat使用了Java
7的父级Dockerfile(默认的最新Java版本是8)。这个Dockerfile设置了CATALINA_HOMEPATH环境变量,然后用mkdir命令新建了CATALINA_HOME目录,WORKDIR指令把当前工作路径更改为CATALINA_HOME,然后RUN指令执行了同一行中一系列的命令:

下载Tomcat压缩包。

下载文件校验码。

验证下载的文件正确。

解压Tomcat压缩包。

删除所有批处理文件(我们是在Linux上运行)。

删除压缩包文件。

把这些命令写在同一行,对应容器来说就是一条命令,最后容器会把执行的结果缓存起来,容器有个策略是检测镜像何时需要重建,以及验证构建过程中的指令是否正确。当一条指令会使镜像更改,容器会把每个步的结果缓存起来,容器能把最上一个正确指令产生的镜像启动起来。

EXPOSE
指令会让容器启动一个容器时暴露指定的端口,正如之前我们启动时那样,我们需要告诉容器哪个物理端口会被映射到容器上(-p
参数),EXPOSE 的作用就是这个定义容器端口。最后 Dockerfile 使用
catalina.sh 脚本启动 Tomcat。

简单回顾

用 Dockerfile 从头开始构建 Tomcat
是一个漫长的过程,我们总结一下目前为止的步骤:

安装 Debian Linux。

安装 curl 和 wget。

安装源码管理工具。

下载并安装 Java。

下载并安装Tomcat。

暴露容器实例的 8080 端口。

用 catalina.sh 启动 Tomcat。

现在你应该成为一个 Dockerfile
专家了,下一步我们将尝试构建一个自定义容器镜像。

部署自定义应用到容器

因为本篇指南主要关注点是如何在容器中部署 Java
应用,而不是应用本身,我会构建一个简单的 Hello World
servlet。你可以从GitHub获取到这个项目,源码并无任何特别,只是一个输出”
Hello World! “的 servlet
。更加有趣的是相应的Dockerfile
,如清单
7 所示。**

代码清单 7. Hello World servlet 的 Dockerfile

sh FROM tomcatADD deploy /usr/local/tomcat/webapps

可能看起来不大一样,但你应该能理解以上代码的作用是:

* FROM tomcat 指明这个 Dockerfile 是基于 Tomcat 镜像构建。

* ADD deploy 告诉容器把本地文件系统中的” deploy “目录,复制到 Tomcat
镜像中的 /usr/local/tomcat/webapps 路径 。

在本地使用 maven 命令编译这个项目:

sh mvn clean install

这样将会生成一个 war 包,target/helloworld.war,把这个文件复制到项目的
docker/deploy 目录(你需要先创建好),最后你要使用上边的 Dockerfile
构建容器镜像,在项目的 docker 目录中执行以下命令:

sh docker build -t lygado/docker-tomcat.

这个命令让容器从当前目录(用点号.表示)构建一个新的镜像,并用” -t
“打上标签 lygado/docker-tomcat,这个例子中,lygado 是我的 DockerHub
用户名, docker-image
是镜像名称(你需要替换成你自己的用户名)。查看是否构建成功你可以执行以下命令:

sh $ docker images

REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE

lygado/docker-tomcat latest ccb455fabad9 42 seconds ago 849.5 MB

最后,你可以用以下命令加载这个镜像:

sh docker run -d -p 8080:8080 lygado/docker-tomcat

这个实例启动之后 ,你可以用以下URL访问(请把 URL 中的 IP
替换成你虚拟机的 IP ):

sh

还是那样,你可以用容器的 ID 来终止这个实例。

Docker push

一旦你构建并测试过了你的容器镜像,你可以把这个镜像推送到你 DockerHub
的账号中:

sh docker push lygado/docker-tomcat

这样,你的镜像就能被全世界访问到了,当然,为了隐私起见,你也可以推送到私有的容器仓库。下面,我们将把容器集成到应用的构建过程,目标是在构建应用完成后,会产出一个包含应用的容器镜像。

把容器集成到 Maven 构建过程

在前边的部分,我们创建了一个自定义的 Dockerfile,并把 WAR
包部署到它里边。这样意味着把 WAR 包从项目的 target 目录,复制到
docker/deploy
目录下,并且从命令行中运行docker。这并没花多少功夫,但如果你需要频繁的改动并测试代码,你会发现这个过程很烦琐。而且,如果你需要在一个
CI 服务器上构建应用,并产出一个容器镜像,那你需要弄明白怎样把容器和 CI
工具整合起来。

现在我们尝试一种更有效的方法,使用 Maven 和 Maven Docker
插件来构建一个容器镜像。

我的用例有这些:

  1. 能创建基于 Tomcat 的容器镜像,以用于部署我的应用。

  2. 能在测试中自行构建。

  3. 能整合到前期集成测试和后期集成测试。

docker-maven-plugin 能满足这些需求,而且易于使用和理解。

关于 Maven Docker插件

这个插件本身有良好的文档,这里特别说明一下两个主要的组件:

在 POM.xml 中配置容器镜像的构建和运行。

描述哪些文件要包含在镜像中。

清单 8 是 POM.xml 中插件的配置,定义了镜像的构建和运行的配置。

代码清单 8. POM 文件的 build 小节, Docker Maven plug-in 配置

xml <build>

<finalName>helloworld</finalName>

<plugins>

<plugin>

<groupId>org.jolokia</groupId>

<artifactId>docker-maven-plugin</artifactId>

<version>0.13.4</version>

<configuration>

<dockerHost>tcp://192.168.99.100:2376</dockerHost>
<certPath>/Users/shaines/.docker/machine/machines/default</certPath>

<useColor>true</useColor>

<images>

<image>

<name>lygado/tomcat-with-my-app:0.1</name>

<alias>tomcat</alias>

<build>

<from>tomcat</from>

<assembly>

<mode>dir</mode

<basedir>/usr/local/tomcat/webapps</basedir

<descriptor>assembly.xml</descriptor>

</assembly>

</build>

<run>

<ports>

<port>8080:8080</port>

</ports>

</run>

</image>

</images>

</configuration>

</plugin>

</plugins>

</build>

正如你所见,这个配置相当简单,包含了以下元素:

Plug-in 定义

groupId, artifactIdversion 这些信息指定要用哪个插件。

全局设置

dockerHostcertPath
元素,定义了容器主机的位置,这些配置会用于启动容器,以及指定容器证书。容器证书的路径在DOCKER_CERT_PATH
环境变量中能看到。

镜像设置

build 元素下的所有 image 元素都定义在images 元素下,每个
image 元素都有镜像相关的配置,与buildrun
的配置一样,主要的配置是镜像的名称,在这个例子中,是我的 DockerHub
用户名 (lygado),镜像的名称 (tomcat-with-my-app) 和镜像的版本号 (
0.1 )。你也可以用 Maven 的属性来定义这些值。

镜像构建配置

一般构建镜像时,我们会使用 docker build 命令,以及一个 Dockerfile
来定义构建过程。Maven Docker 插件也允许你使用
Dockerfile,但在例子中,我们使用一个运行时生成在内存中的 Dockerfile
来构建。因此,我们在 from 元素中定义父级镜像,这个例子中是
tomcat,然后在 assembly 中作其他配置。

使用 Maven 的
maven-assembly-plugin,可以定义一个项目的输出内容,指定包含依赖,模块,文档,或其他文件到一个独立分发的包中。docker-maven-plugin
继承了这个标准,在这个例子中,我们选择了 dir 模式,也就是说定义在
src/main/docker/assembly.xml 中的文件会被拷贝到容器镜像中的 basedir
中。其他模式还有 tartgzzipbasedir
元素中定义了放置文件的路径,这个例子中是Tomcat 的 webapps 目录。

最后,descriptor 元素指定了 assembly 文件,这个文件位于 basedir
中定义的 src/main/docker
中。以上是一个很简单的例子,我建议你通读一下相关文档,特别地,可以了解
entrypointcmd
元素,这两个元素可以指定启动容器镜像的命令,env元素可以指定环境变量,runCmds
元素类似 Dockerfile 中的RUN 指令,workdir
元素可以指定工作路径,volumes
元素可以指定要挂载的磁盘卷。简言之,这个插件实现了所有Dockerfile
中所需要的语法,所以前面所用到的 Dockerfile 指令都可以在这个插件中使用。

镜像运行配置

启动容器镜像时会用到 docker run
命令,你可以传一些参数给容器。这个例子中,我们要用
docker run -d -p 8080:8080 lygado/tomcat-with-my-app:0.1
这个命令启动镜像,所以我们只需要指定一下端口映射。

run 元素可以让我们指定所有运行时参数,所以我们指定了把容器中的 8080
映射到容器宿主机的 8080。另外,还可以在 run
这节中指定要挂载的卷(使用volumes),或者要链接起来的容器(使用links)。docker:start在集成测试中很常用,在
run 小节中,我们可以使用 wait
参数来指定一个时间周期,这样就可以等待到某个日志输出,或者一个 URL
可用时,才继续执行下去,这样就可以保证在集成测试开始前镜像已经运行起来了。

加载依赖

src/main/docker/assembly.xml
文件定义了哪些文件需要复制到容器镜像中,如清单 9 所示:

清单 9. assembly.xml

xml <assembly
xmlns=””

xmlns:xsi=””

xsi:schemaLocation=”
;

<dependencySets>

<dependencySet>

<includes>

<include>com.geekcap.vmturbo:hello-world-servlet-example</include>

</includes>

<outputDirectory>.</outputDirectory>

<outputFileNameMapping>helloworld.war</outputFileNameMapping>

</dependencySet>

</dependencySets>

</assembly>

在清单 9 中,我们可以看到包含 hello-world-servlet-example
在内的一个依赖集合,以及复制的目标路径,outputDirectory
这个路径是相对于前面提到的 basedir 的,也就是 Tomcat 的 webapps 目录。

这个插件有以下几个 Maven targets:

  1. docker:build: 构建镜像

  2. docker:start: 启动镜像

  3. docker:stop: 停止镜像

  4. docker:push: 把镜像推送到镜像仓库,如DockerHub

  5. docker:remove: 本地删除镜像

  6. docker:logs: 输出容器日志

构建镜像

你可以从GitHub中获取源码,然后用下边的命令构建:

sh mvn clean install

用以下命令构建容器镜像:

sh mvn clean package docker:build

一旦镜像构建成功,你可以在 docker images 的返回结果中看到:

sh $ docker images

REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE

lygado/tomcat-with-my-app 0.1 1d49e6924d19 16 minutes ago 347.7 MB

可以用以下命令启动镜像:

sh mvn docker:start

现在可以在docker ps中看到已经启动了,然后可以通过以下URL访问:

sh

最后,可以用以下命令停止容器:

sh mvn docker:stop

总结

容器是一种使进程虚拟化的容器技术,它提供了一系列容器客户端命令来调用容器守护进程。在
Linux 上,容器守护进程可以直接运行于 Linux 操作系统,但是在 Windows 和
Mac 上,需要有一个 Linux
虚拟机来运行容器守护进程。容器镜像包含了一个轻量级的操作系统,还额外包含了应用运行的依赖库。容器镜像由
Dockerfile 定义,可以在 Dockerfile 中包含一系列配置镜像的指令。

在这个开源 Java 项目指南中,我介绍了容器的基础,讲解了
CentOS、Java、Tomcat 等镜像的 Dockerfile 细节,并演示了如何用 Tomcat
镜像来构建新的镜像。最后,我们使用 docker-maven-plugin 来把容器集成到
Maven 的构建过程中。通过这样,使得测试更加简单了,还可以把构建过程配置在
CI 服务器上部署到生产环境。

本文中的示例应用非常简单,但是涉及的构建步骤同样可以用在更复杂的企业级应用中。好好享受容器带给我们的乐趣吧。

发表评论

电子邮件地址不会被公开。 必填项已用*标注