开篇词

该指南将引导你构建用于运行 Spring Boot 应用的 Docker 镜像。
 

你将创建的应用

Docker 是具有 “社交” 方面的 Linux 容器管理工具箱,允许用户发布容器镜像并使用其他人发布的镜像。Docker 镜像是运行容器化进程的秘诀,在该指南中,我们将为一个简单的 Spring Boot 应用构建一个镜像。

还有一个以 Docker 为主题的指南(尽请期待~),其中涵盖的选项比该处的多,并且更加详细。

 

你将需要的工具

  • 大概 15 分钟左右;
  • 你最喜欢的文本编辑器或集成开发环境(IDE)
  • JDK 1.8 或更高版本;
  • Gradle 4+Maven 3.2+
  • 你还可以将代码直接导入到 IDE 中:

如果我们使用的不是 Linux 系统,则需要一个虚拟服务器。通过安装 VirtualBox,Mac 的 boot2docker 等其他工具可以为我们无缝管理它。访问 VirtualBox 的下载站点,然后为我们的计算机选择版本。下载并安装。不用担心实际运行它。

我们还需要只能在 64 位系统上运行的 Docker。有关为我们的机器搭建 Docker 的详细信息,请参见 https://docs.docker.com/installation/#installation。在继续进行之前,请确认我们可以从 Shell 运行 docker 命令。如果我们使用的是 boot2docker,则需要先运行它。
 

如何完成这个指南

像大多数的 Spring 入门指南一样,你可以从头开始并完成每个步骤,也可以绕过你已经熟悉的基本设置步骤。如论哪种方式,你最终都有可以工作的代码。

  • 要从头开始,移步至用 Gradle 来构建
  • 要跳过基础,执行以下操作:
  • 下载并解压缩该指南将用到的源代码,或借助 Git 来对其进行克隆操作:git clone https://github.com/spring-guides/gs-spring-boot-docker.git
  • 切换至 gs-spring-boot-docker/initial 目录;
  • 跳转至该指南的搭建 Spring Boot 应用

待一切就绪后,可以检查一下 gs-spring-boot-docker/complete 目录中的代码。
 

用 Gradle 来构建

首先,我们设置一个基本的构建脚本。在使用 Spring 构建应用时可以使用任何喜欢的构建系统,但此处包含使用 GradleMaven 所需的代码。如果你都不熟悉,请参阅使用 Gradle 构建 Java 项目使用 Maven 构建 Java 项目

创建目录结构

在我们选择的项目目录中,创建以下自目录结构;例如,在 *nix 系统上使用 mkdir -p src/main/java/hello

└── src
    └── main
        └── java
            └── hello

创建 Gradle 构建文件

以下是初始 Gradle 构建文件
build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.2.1.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
    baseName = 'gs-spring-boot-docker'
    version =  '0.1.0'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

Spring Boot gradle 插件提供了许多方便的功能:

  • 它收集类路径上的所有 jar,并构建一个可运行的单个超级 jar,这使执行和传输服务更加方便;
  • 它搜索 public static void main() 方法并将其标记为可运行类;
  • 它提供了一个内置的依赖解析器,用于设置版本号以及匹配 Spring Boot 依赖。我们可以覆盖所需的任何版本,但默认为 Boot 选择的一组版本。
     

用 Maven 来构建

首先,我们搭建一个基本的构建脚本。使用 Spring 构建应用时,可以使用任何喜欢的构建系统,但是此处包含了使用 Maven 所需的弟阿玛。如果你不熟悉 Maven,请参阅使用 Maven 构建 Java 项目

创建目录结构

在我们选择的项目目录中,创建以下自目录结构;例如,在 *nix 系统上使用 mkdir -p src/main/java/hello

└── src
    └── main
        └── java
            └── hello

创建 Maven 构建文件

以下是初始 Maven 构建文件。
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-spring-boot-docker</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Spring Boot Maven 插件提供了许多方便的功能:

  • 它收集类路径上的所有 jar,并构建一个可运行的单个超级 jar,这使执行和传输服务更加方便;
  • 它搜索 public static void main() 方法并将其标记为可运行类;
  • 它提供了一个内置的依赖解析器,用于设置版本号以及匹配 Spring Boot 依赖。我们可以覆盖所需的任何版本,但默认为 Boot 选择的一组版本。
     

用 IDE 来构建

  • 阅读如何将该指南直接导入 Spring Tool Suite
  • 阅读如何在 IntelliJ IDEA 尽情期待~ 中使用该指南。
     

搭建 Spring Boot 应用

现在,我们可以创建一个简单的应用。
src/main/java/hello/Application.java

package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class Application {

  @RequestMapping("/")
  public String home() {
    return "Hello Docker World";
  }

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

}

该类被标记为 @SpringBootApplication@RestController,这意味着 Spring MVC 已准备好使用该类来处理 Web 请求。@RequestMapping/ 映射到 home() 方法,该方法仅发送 “Hello World” 响应。main() 方法使用 Spring Boot 的 SpringApplication.run() 方法启动应用。

现在我们可以在没有 Docker 容器的情况下(即在主机 OS 中)运行应用。

如果我们使用的是 Gradle,请执行:

./gradlew build && java -jar build/libs/gs-spring-boot-docker-0.1.0.jar

如果我们使用的是 Maven,请执行:

./mvnw package && java -jar target/gs-spring-boot-docker-0.1.0.jar

并前往 localhost:8080 以查看 “Hello Docker World” 消息。
 

使其容器化

Docker 有一个简单的 “Dockerfile” 文件格式,用于指定镜像的 “层”。因此,让我们继续并在我们的 Spring Boot 项目中创建一个 Dockerfile:
Dockerfile

FROM openjdk:8-jdk-alpine
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

我们可以使用以下命令运行它(如果使用的是 Maven):

docker build -t springio/gs-spring-boot-docker .

或者(如果我们使用的是 Gradle):

docker build --build-arg JAR_FILE=build/libs/*.jar -t springio/gs-spring-boot-docker .

该命令生成一个镜像并将其标记为 springio/gs-spring-boot-docker

这个 Dockerfile 非常简单,但这就是运行 Spring Boot 应用所需的一切,没有多余的装饰:仅 Java 和 JAR 文件。构建将创建一个 spring 用户和一个 spring 组来运行该应用。然后,它将项目 JAR 文件作为 “app.jar” COPY 到容器中,该文件将在 ENTRYPOINT 中执行。使用 Dockerfile ENTRYPOINT 的数组形式,因此没有 shell 包装 Java 进程。有关 Docker 的主题指南(尽请期待~)对该主题进行了更详细的介绍。

为了减少 Tomcat 启动时间,我们预先添加了一个系统属性,该属性指向 “/dev/urandom” 作为熵的涞源。对于 JDK 8 或更高版本,这不再是必须的。

使用用户权限运行应用有助于减轻某些风险(例如,参见 StackExchange 上的一个线索)因此,对 Dockerfile 的一项重要改进是以非 root 用户身份运行该应用:
Dockerfile

FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

同样,为了利用 Spring Boot 的胖 Jar 文件中的依赖关系和应用资源之间的清晰分隔,我们将使用稍微不同的 Dockerfile 实现:
Dockerfile

FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG DEPENDENCY=target/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

该 Dockerfile 有一个 DEPENDENCY 参数,该参数指向我们解开的胖 jar 的目录。从 Maven 构建:

$ mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

或者从 Gradle 构建:

$ mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

如果我们做对了,则它已经包含一个包含依赖的 jar 的 BOOT-INF/lib 目录,以及一个其中包含应用类的 BOOT-INF/classes 目录。请注意,我们正在使用应用自己的主类 hello.Application(这比使用胖 jar 启动器提供的间接调用要快)。

分解 jar 文件可能会导致类路径在运行时的顺序不同。行为良好且编写良好的应用不必理会该问题,但如果不仔细管理依赖,则可能会看到行为更改。

如果我们使用的是 boot2docker,则需要先运行它,然后再使用 Docker 命令行构建工具执行任何操作(它会运行一个守护进程,该进程为我们在虚拟机中处理工作)。

要构建镜像,我们可以使用 Docker 命令行。例如:

docker build -t springio/gs-spring-boot-docker .

从 Gradle 构建,添加显式构建参数:

docker build --build-arg DEPENDENCY=build/dependency -t springio/gs-spring-boot-docker .

当然,如果仅使用 Gradle,则只需更改 Dockerfile,以使 DEPENDENCY 的默认值与解压缩的归档文件位置匹配。

我们可能不想使用 Docker 命令行进行构建,而是要使用构建插件。Google 有一个名为 Jib 的开源工具,该工具有 Maven 和 Gradle 插件。可能最有趣的是我们不需要 docker - 它使用与从 docker build 获得的相同的标准输出来构建镜像,但除非我们要求,否则不使用 docker - 因此在有无 docker 的环境中,它都能工作(在构建服务器中并不罕见)。

使用 Maven 构建 Docker 镜像

为了快速入门,我们可以运行 Jib 甚至不更改 pom.xml:

./mvnw com.google.cloud.tools:jib-maven-plugin:dockerBuild -Dimage=springio/gs-spring-boot-docker

要推送到 Docker 注册表,请使用 build 目标,而不是 dockerBuild,例即:

./mvnw com.google.cloud.tools:jib-maven-plugin:build -Dimage=springio/gs-spring-boot-docker

为此,我们将需要拥有推送到 Dockerhub 的权限,默认情况下我们没有该权限。将镜像前缀更改为我们自己的 Dockerhub ID,并 docker login 以确保在运行 Maven 之前已通过身份验证。

使用 Gradle 构建 Docker 镜像

如果我们使用的是 Gradle,则需要添加一个新的插件,如下所示:
build.gradle

plugins {
  ...
  id 'com.google.cloud.tools.jib' version '1.8.0'
}

或使用入门指南中使用的经典方式:
build.gradle

buildscript {
    repositories {
      maven {
        url "https://plugins.gradle.org/m2/"
      }
      mavenCentral()
    }
    dependencies {
        classpath('org.springframework.boot:spring-boot-gradle-plugin:2.2.1.RELEASE')
        classpath('com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin:1.8.0')
    }
}
apply plugin: 'com.google.cloud.tools.jib'

我们可以在一条命令中使用 Gradle 构建标记的 docker 镜像:

./gradlew jibDockerBuild --image=springio/gs-spring-boot-docker

与 Maven 构建一样,还有一个构建任务可以构建并推送到 Docker 注册表:

./gradlew jib --image=springio/gs-spring-boot-docker

如果我们已在命令上通过 docker 进行了身份验证,则镜像推送将从我们的本地 ~/.docker 配置进行身份验证。

推送之后

示例中的 “docker push”(或使用 “jib” 构建插件)将对我们失败(除非我们是 Dockerhub 的 “springio” 组织的一部分),但是如果我们更改配置以匹配我们自己的 docker ID,则它应该会成功,我们将拥有一个新的标记的已部署镜像。

我们无需向 docker 注册或发布任何内容即可运行在本地构建的 docker 镜像。如果我们是使用 Docker 构建的(通过命令行或 Jib),则仍然有一个本地标记的镜像,可以像这样运行它:

docker run -p 8080:8080 -t springio/gs-spring-boot-docker
2015-03-31 13:25:48.035  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-03-31 13:25:48.037  INFO 1 --- [           main] hello.Application                        : Started Application in 5.613 seconds (JVM running for 7.293)

然后可以在 http://localhost:8080 上找到该应用(访问该应用,并显示 “Hello Docker World”)。

当将 Mac 与 boot2docker 结合使用时,通常会在启动时看到如下内容:

Docker client to the Docker daemon, please set:
    export DOCKER_CERT_PATH=/Users/gturnquist/.boot2docker/certs/boot2docker-vm
    export DOCKER_TLS_VERIFY=1
    export DOCKER_HOST=tcp://192.168.59.103:2376

要查看应用,我们必须访问 DOCKER_HOST 中的 IP 地址而不是 localhost。在这种情况下,https://192.168.59.103:8080,虚拟机面向公众的 IP。

当它运行时,我们可以在容器列表中看到,例如:

docker ps
CONTAINER ID        IMAGE                                   COMMAND                  CREATED             STATUS              PORTS                    NAMES
81c723d22865        springio/gs-spring-boot-docker:latest   "java -Djava.secur..."   34 seconds ago      Up 33 seconds       0.0.0.0:8080->8080/tcp   goofy_brown

并再次关闭它,我们可以 docker stop 后跟上面列表中的容器 ID(我们将有所不同):

docker stop goofy_brown
81c723d22865

如果我们愿意,还可以在完成后删除该容器(该容器保留在 /var/lib/docker 下的文件系统中):

docker rm goofy_brown

使用 Spring 配置

使用 Spring 配置运行刚创建的 Docker 镜像就像环境变量传递给 Docker run 命令一样容易:

docker run -e "SPRING_PROFILES_ACTIVE=prod" -p 8080:8080 -t springio/gs-spring-boot-docker

或者

docker run -e "SPRING_PROFILES_ACTIVE=dev" -p 8080:8080 -t springio/gs-spring-boot-docker

调试 Docker 容器中的应用

要调试应用,可以使用 JPDA Transport。因此,我们将容器视为远程服务器。要启用该功能,请在容器运行期间在 JAVA_OPTS 变量中传递 Java 代理设置,并将代理的端口映射到 localhost。使用 Docker for Mac 存在局限性,因为如果不使用黑魔法,我们就无法通过 IP 访问容器。

 

概述

恭喜你!我们刚刚为 Spring Boot 应用创建了 Docker 容器!Spring Boot 应用默认在容器内的 8080 端口上运行,我们在命令行上使用 “-p” 将其映射到主机上的同一端口。
 

参见

以下指南也可能会有所帮助:

  • 使用 Spring MVC 服务 Web 内容
  • 使用 Spring Boot 构建应用