Spring-Boot-with-Docker

本文介绍如何构建Spring Boot应用的Docker镜像。Docker是一个具有“社交”特性的linux容器管理的工具箱,允许用户发布和使用他人发布的容器镜像,一个Docker镜像就是一个容器化进程,本文介绍如何构建一个Spring Boot应用镜像。

基本的Dockerfile

一个Spring Boot应用很容易制作一个可执行的JAR文件,比如Maven可以使用mvn install,Gradle可以使用gradle build构建,制作可执行JAR的一个基本的Dockerfile类似这样,文件放在项目的顶级目录:

1
2
3
4
5
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

JAR_FILE可以作为docker命令的一部分作为参数传入(对于Maven、Gradle是不同的),比如对于Maven:

$ docker build --build-arg=target/*.jar -t myorg/myapp .

对于Gradle:

$ docker build --build-arg=build/libs/*.jar -t myorg/myapp .

当然,一旦确定了构建系统,你可以不需要ARG,直接硬编码。比如对于Maven:

1
2
3
4
FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

然后可以简化构建命令:

$ docker build -t myorg/myapp .

运行镜像:

1
2
3
4
5
6
7
8
9
10
11
$ docker -p 8080:8080 myorg/myapp
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.6.RELEASE)

2019-11-18 03:52:01.912 INFO 1 --- [ main] hello.Application : Starting Application v0.1.0 on 12cf47053074 with PID 1 (/app.jar started by root in /)
...

如果你想看看镜像的内部结构,可以执行:

1
2
3
4
5
docker run -ti --entrypoint /bin/sh myorg/myapp
/ # ls
app.jar dev home media opt root sbin sys usr
bin etc lib mnt proc run srv tmp var
/ #

Entry Point

Dockerfile的执行方式使用ENTRYPOINT而没有使用shell打包java进程,这样做的好处是java进程可以响应KILL信号指令,比如在本例中使用CTRL-C中止进程。如果ENTRYPOINT的命令比较长,可以单独制作一个shell脚本,并将脚本拷贝到镜像:

1
2
3
4
5
FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY run.sh .
COPY target/*.jar app.jar
ENTRYPOINT ["run.sh"]

run.sh

1
2
#!/bin/sh
exec java -jar /app.jar

ENTRYPOINT还可以注入环境变量,比如加入运行时java命令行参数:

1
2
3
4
5
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
$ docker build --build-arg JAR_FILE=./target/gs-spring-boot-docker-0.1.0.jar  -t ljun51/docker .
$ docker run -p 8080:8080 -e "JAVA_OPTS=-Ddebug -Xmx128m" ljun51/docker

上面的示例以Spring Boot的-Ddebug参数输出DEBUG日志。

上面的示例使用ENTRYPOINT并带有明确的shell可以传递环境变量的参数给java command,但是不能传递命令行参数给Spring Boot应用。下面这样修改端口不会生效:

$ docker run -p 9000:9000 ljun51/docker --server.port=9000

不生效的原因是docker命令的--server.port=9000部分传给了ENTRYPOINT(sh),而没有传给它启动的java进程。要修复这个问题可以通过添加CMD

1
2
3
4
5
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar ${0} ${@}"]
$ docker build --build-arg JAR_FILE=./target/gs-spring-boot-docker-0.1.0.jar  -t ljun51/docker .
$ docker run -p 9000:9000 ljun51/docker --server.port=9000

${0}表示“command”(第一个参数),${@}表示“command arguments”(命令行其他参数)。如果使用shell脚本,则不需要${0}.run.sh:

1
2
#!/bin/sh
exec java ${JAVA_OPTS} -jar /app.jar ${@}

到目前为止,docker配置都比较简单,生成的镜像也不是非常高效。docker镜像在JAR中打包了一个单独的文件系统层,它的大小在10MB以上,对于某些应用甚至50MB以上,我们可以通过分离成多层来改进。

使用工具构建镜像

编写一个基本的Spring Boot应用

创建一个简单的应用,src/main/java/hello/Application.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}

}

现在可以运行这个应用,而不必有Docker容器,使用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文件格式指定镜像的“layers”,在Spring Boot工程的顶级目录下创建一个Dockerfile文件,文件名就叫Dockerfile

1
2
3
4
5
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

这个Dockerfile非常简单但是包含了运行Spring Boot应用需要的内容:Java和JAR文件。项目JAR文件被COPY到容器中,并叫”app.jar”,然后执行ENTRYPOINT,没有shell包裹java进程。

文件中添加了一个指向”/tmp”的VOLUME,是因为默认情况下Spring Boot应用在该目录中创建工作目录。实际结果是在主机上的“/var/lib/docker”下创建一个临时文件,并将其链接到“/tmp”下的容器。对于我们在此处编写的简单应用程序,此步骤是可选的,但对于其他Spring Boot应用程序,如果它们需要实际在文件系统中进行写操作,则可能是必需的。

为减少Tomcat启动时间,添加了一个系统属性指向了"/dev/urandom"作为熵的来源,如果使用的是较新的Spring Boot或Tomcat的标准版本,这不是必须的。

为了利用Spring Boot胖JAR文件中的依赖项和应用程序资源之间的明确分割,我们将只要稍微不同的Dockerfile实现:

1
2
3
4
5
6
7
FROM openjdk:8-jdk-alpine
VOLUME /tmp
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"]

使用Maven构建Docker镜像

在Maven的pom.xml新增插件信息,更多信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<properties>
<docker.image.prefix>ljun51</docker.image.prefix>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.9</version>
<configuration>
<repository>${docker.image.prefix}/${project.artifactId}</repository>
</configuration>
</plugin>
</plugins>
</build>

该配置指定一项强制性的内容:有镜像名的仓库,镜像以ljun51/gs-spring-boot-docker命名。

其他可选属性:

  • 解压的fat jar的目录名,作为构建docker镜像的参数可以通过<buildArgs/>插件配置指定。
  • 镜像标签,如果未指定默认使用”latest”,可以通过<tag/>元素设置。

为了确保docker镜像创建之前JAR包被解压,添加下面的插件依赖配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack</id>
<phase>package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>

使用命令行构建docker镜像:

$ ./mvnw install dockerfile:build

推送镜像到dockhub,./mvnw dockerfile:push。Maven运行install时自动推送镜像的配置:

1
2
3
4
5
6
7
8
9
10
<executions>
<execution>
<id>default</id>
<phase>install</phase>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>

使用Gradle构建Docker镜像

如果使用Gradle需要这样添加插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
buildscript {
...
dependencies {
...
classpath('gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0')
}
}

group = 'ljun51'

...
apply plugin: 'com.palantir.docker'

task unpack(type: Copy) {
dependsOn bootJar
from(zipTree(tasks.bootJar.outputs.files.singleFile))
into("build/dependency")
}
docker {
name "${project.group}/${bootJar.baseName}"
copySpec.from(tasks.unpack.outputs).into("dependency")
buildArgs(['DEPENDENCY': "dependency"])
}

这个配置说明4个事情:

  • 解压fat jar文件
  • 创建的镜像名为ljun51/gs-spring-boot-docker
  • 解压jar file的位置,可以使用硬编码
  • 指向jar file的构建参数

使用Gradle构建docker镜像并推送到dockerhub:

$ ./gradlew build docker

如果没有dockerhub的账号,推送应该会报错;推送的的步骤不是必须的,即使没有推送也是可以使用docker运行的:

1
2
3
4
$ docker run -p 8080:8080 -t ljun51/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)

查看正在运行的docker容器:

1
2
3
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
01cae1671836 ljun51/gs-spring-boot-docker "java -cp app:app/li…" 22 seconds ago Up 21 seconds 0.0.0.0:8080->8080/tcp elated_cori

通过上面的容器ID停止运行:

1
2
$ docker stop 01cae1671836
01cae1671836

使用Spring Profiles

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

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

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

在Docker容器中调试应用

可以使用JPDA Transport像调试远程服务一样。使用JAVA_OPTS环境变量传递java agent设置启用这个功能,映射agent端口到本机。使用Docker for Mac会有一些限制,可以通过一些黑魔法解决。

$ docker run -e "JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n" -p 8080:8080 -p 5005:5005 -t ljun51/gs-spring-boot-docker

参考:

  1. Spring Boot with Docker
  2. Spring Boot Docker