Spring Boot 与 Docker 结合

Docker 是一个具有社交倾向的 Linux 容器管理工具包,允许用户发布容器镜像,其他用户可以使用这些镜像。Docker 镜像是运行容器化进程的基础,本文将介绍如何编译一个简单的 Spring Boot 应用的镜像。

Docker 的安装和基本使用不在本文中介绍,之后可以单独拿出来写一写。

本文将使用 Gradle 作为编译工具,基础项目工程直接使用 IDEA 的 Spring Initializr 生成,如下图:

直接下一步,注意这里的 Type 修改为 Gradle Project,然再下一步

然后只需要勾选 Web 即可

我们来简单调整一下 build.gradle,新增:

1
2
3
4
jar {
baseName = 'my-spring-boot-docker'
version = '0.1.0'
}

它的作用是让编译出来的 jar 包文件名为:my-spring-boot-docker-0.1.0.jar

此时 build.gradle 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
jar {
baseName = 'my-spring-boot-docker'
version = '0.1.0'
}
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}

然后我们写一个最简单的 Controller,为了方便直接写在 main 方法的类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.jpanj;
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 SpringBootDockerApplication {
@RequestMapping("/")
public String home() {
return "Hello Docker World";
}
public static void main(String[] args) {
SpringApplication.run(SpringBootDockerApplication.class, args);
}
}

现在我们在不使用 Docker 容器 的情况下运行这个应用:

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

然后访问 localhost:8080 可以看到 Hello Docker World 的返回结果。

接下来让我们把它容器化吧

Docker 有一个简单的 Dockerfile 文件格式用来指定生成镜像的层次,所以我们在 Spring Boot 项目中创建一个 Dockerfile,将这个文件放在项目根目录下即可,现在这个项目结构是这个样的:

Dockerfile 内容如下:

1
2
3
4
5
FROM frolvlad/alpine-oraclejdk8:slim
VOLUME /tmp
ADD target/my-spring-boot-docker-0.1.0.jar app.jar
ENV JAVA_OPTS=""
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]

这个 Dockerfile 非常简单,不过这就是你运行一个 Spring Boot 应用的全部了,只需要 JavaJAR 文件就够了。这个项目 JAR 文件被作为 app.jar 加入到容器中,然后通过 ENTRYPOINT 来执行它。

Docker容器 运行时应该尽量保持容器存储层不发生写操作,在这里我们添加一个 VOLUME 指向 /tmp 是因为 Spring Boot 应用在默认情况下会为 Tomcat 创建工作目录。这里的 /tmp 目录就会在运行时自动挂载为匿名卷,任何向 /tmp 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。

当然,也可以在运行时可以覆盖这个挂载设置

docker run -d -v mytmp:/tmp xxxx

在这行命令中,就使用了 mytmp 这个命名卷挂载到了 /tmp 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

不过此步骤在这个简单的应用中是可选的,但是在其他会写入文件系统的 Spring Boot 应用中是必须的。

接下来就是把这个项目编译成一个可以到处运行的 Docker 镜像了,在 build.gradle 中我们添加一些新的插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
buildscript {
...
dependencies {
...
classpath('se.transmode.gradle:gradle-docker:1.2')
}
}
...
apply plugin: 'docker'
task buildDocker(type: Docker, dependsOn: build) {
applicationName = jar.baseName
dockerfile = file('Dockerfile')
doFirst {
copy {
from jar
into "${stageDir}/target"
}
}
}

上边的配置做了这 3 件事:

  • 镜像的名字被设置为 jar 配置的 baseName 属性
  • 确定 Dockerfile 的位置
  • jar 文件从编译目录复制到 docker 编译目录的 target 目录下,这就是我们在 Dockerfile 中看到的 ADD target/my-spring-boot-docker-0.1.0.jar app.jar 为什么会生效的原因。

此时完整的 build.gradle 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath('se.transmode.gradle:gradle-docker:1.2')
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'docker'
jar {
baseName = 'my-spring-boot-docker'
version = '0.1.0'
}
task buildDocker(type: Docker, dependsOn: build) {
applicationName = jar.baseName
dockerfile = file('Dockerfile')
doFirst {
copy {
from jar
into "${stageDir}/target"
}
}
}
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}

现在你可以使用下边的命令编译这个 docker 镜像,然后将它推送到远端仓库中分享给其他用户使用(本教程不介绍推送远端仓库的方法)。

./gradlew build buildDocker

执行上边命令后,可以在本地的 docker 镜像仓库中看到,已经有了我们自己编译出来的 my-spring-boot-docker 镜像:

然后你就可以像这样来运行它了:

docker run -p 8080:8080 -t my-spring-boot-docker:0.0.1-SNAPSHOT

这里说明一下 -p 的用途, -p 8080:8080 的意思是将本地的 8080 端口映射到容器的 8080 端口,因为容器相对于主机来说是完全隔离的,所以必须要有此设置,不然外部是无法访问到 8080 端口的。

现在再次访问 localhost:8080 就可以看到 Hello Docker World 啦。

当容器运行时你可以通过 docker ps 的命令看到正在运行的容器列表:

而且可以通过 docker stop 加上这个容器的 ID 来停掉它:

如果你想删掉这个容器,可以使用 docker rm + 容器 ID

到此你已经为 Spring Boot 应用创建了一个 docker 容器,默认运行在容器内的 8080 端口上,我们在命令行中使用 -p 参数将它映射到主机的相同端口上。