Spring Boot 與 Docker

本指南將引導您完成為執行 Spring Boot 應用構建 Docker 映象的過程。我們從一個基本的 Dockerfile 開始,並做一些調整。然後,我們將展示一些使用構建外掛(針對 Maven 和 Gradle)而不是 docker 的選項。這是一個“入門”指南,因此範圍僅限於一些基本需求。如果您要構建用於生產環境的容器映象,則需要考慮很多因素,而本簡短指南無法涵蓋所有內容。

還有一個關於 Docker 的專題指南,它涵蓋了比本指南更廣泛的選擇,並且細節更豐富。

您將構建什麼

Docker 是一個 Linux 容器管理工具集,具有“社交”方面,允許使用者釋出容器映象並使用他人釋出的映象。Docker 映象是一個執行容器化程序的配方。在本指南中,我們將為一個簡單的 Spring Boot 應用構建一個 Docker 映象。

您將需要什麼

如果您沒有使用 Linux 機器,則需要一個虛擬化伺服器。如果您安裝 VirtualBox,則 Mac 的 boot2docker 等其他工具可以無縫地為您管理它。訪問 VirtualBox 下載網站並選擇適合您機器的版本。下載並安裝。無需擔心實際執行它。

您還需要 Docker,它僅在 64 位機器上執行。有關在您的機器上設定 Docker 的詳細資訊,請參閱 https://docs.container.club.tw/installation/#installation。在繼續之前,請驗證您可以在 shell 中執行 docker 命令。如果您使用 boot2docker,您需要首先執行它。

從 Spring Initializr 開始

您可以使用這個預初始化的專案並點選 Generate 下載 ZIP 檔案。此專案已配置為適合本教程中的示例。

手動初始化專案

  1. 導航到 https://start.spring.io。此服務會獲取應用所需的所有依賴項,併為您完成大部分設定。

  2. 選擇 Gradle 或 Maven 以及您想使用的語言。本指南假設您選擇了 Java。

  3. 點選 Dependencies 並選擇 Spring Web

  4. 點選 Generate

  5. 下載生成的 ZIP 檔案,它是根據您的選擇配置的 Web 應用的壓縮包。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成此過程。
您也可以從 Github fork 專案並在您的 IDE 或其他編輯器中開啟它。

設定一個 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 容器的情況下(即在主機作業系統中)執行應用

如果您使用 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

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

如果您使用 Gradle,可以使用以下命令執行它

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

如果您使用 Maven,可以使用以下命令執行它

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

此命令會構建一個映象並將其標記為 springio/gs-spring-boot-docker

這個 Dockerfile 非常簡單,但它是執行一個沒有額外依賴的 Spring Boot 應用所需的全部:只需要 Java 和一個 JAR 檔案。構建過程會建立一個 spring 使用者和一個 spring 組來執行應用。然後將專案 JAR 檔案複製(透過 COPY 命令)到容器中作為 app.jar,並在 ENTRYPOINT 中執行。Dockerfile ENTRYPOINT 的陣列形式被使用,以便 Java 程序沒有 shell 包裝。關於 Docker 的專題指南更詳細地討論了此主題。

為了減少 Tomcat 啟動時間,我們過去常常新增一個系統屬性指向 /dev/urandom 作為熵源。對於 JDK 8 或更高版本,這不再是必需的。

以使用者許可權執行應用有助於減輕一些風險(例如,請參閱 StackExchange 上的一個討論)。因此,對 Dockerfile 的一個重要改進是以非 root 使用者身份執行應用

示例 2. 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"]

構建並執行應用時,您可以在應用啟動日誌中看到使用者名稱

docker build -t springio/gs-spring-boot-docker .
docker run -p 8080:8080 springio/gs-spring-boot-docker

請注意第一個 INFO 日誌條目中的 started by

 :: Spring Boot ::        (v2.2.1.RELEASE)

2020-04-23 07:29:41.729  INFO 1 --- [           main] hello.Application                        : Starting Application on b94c86e91cf9 with PID 1 (/app started by spring in /)
...

此外,在 Spring Boot fat JAR 檔案中,依賴項與應用資源之間存在清晰的分離,我們可以利用這一點來提高效能。關鍵是在容器檔案系統中建立層。這些層在構建時和執行時(在大多數執行時中)都會被快取,因此我們希望變化最頻繁的資源(通常是應用本身的類和靜態資源)在變化較慢的資源之後分層。因此,我們使用稍微不同的 Dockerfile 實現

示例 3. 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 引數,指向我們解壓 fat JAR 的目錄。要將 DEPENDENCY 引數與 Gradle 一起使用,執行以下命令

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

要將 DEPENDENCY 引數與 Maven 一起使用,執行以下命令

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

如果我們做對了,它會包含一個包含依賴 JAR 的 BOOT-INF/lib 目錄,以及一個包含應用類的 BOOT-INF/classes 目錄。請注意,我們使用應用自己的主類:hello.Application。(這比使用 fat JAR 啟動器提供的間接方式更快。)

解壓 JAR 檔案可能導致執行時類路徑順序不同。一個行為良好且編寫良好的應用不應該關心這一點,但如果依賴項管理不仔細,您可能會看到行為變化。
如果您使用 boot2docker,您需要首先執行它,然後才能使用 Docker 命令列或構建工具(它會在虛擬機器中執行一個守護程序為您處理工作)。

從 Gradle 構建中,您需要在 Docker 命令列中新增顯式的構建引數

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

要在 Maven 中構建映象,可以使用更簡單的 Docker 命令列

docker build -t springio/gs-spring-boot-docker .
如果您只使用 Gradle,您可以更改 Dockerfile 以使 DEPENDENCY 的預設值與解壓存檔的位置匹配。

除了使用 Docker 命令列構建之外,您可能希望使用構建外掛。Spring Boot 透過使用其自身的構建外掛支援從 Maven 或 Gradle 構建容器。Google 還有一個名為 Jib 的開源工具,它具有 Maven 和 Gradle 外掛。也許這種方法最有趣的地方在於您不需要 Dockerfile。您可以使用與 docker build 相同的標準容器格式構建映象。此外,它可以在未安裝 Docker 的環境中使用(在構建伺服器中並不少見)。

預設情況下,由預設 buildpacks 生成的映象不會以 root 使用者身份執行您的應用。請檢視 GradleMaven 的配置指南,瞭解如何更改預設設定。

使用 Gradle 構建 Docker 映象

您可以使用一個命令透過 Gradle 構建帶標籤的 Docker 映象

./gradlew bootBuildImage --imageName=springio/gs-spring-boot-docker

使用 Maven 構建 Docker 映象

為了快速開始,您甚至無需修改 pom.xml 即可執行 Spring Boot 映象生成器(請記住,Dockerfile 如果仍然存在,則會被忽略)

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=springio/gs-spring-boot-docker

要推送到 Docker 倉庫,您需要有推送許可權,預設情況下您沒有此許可權。將映象字首更改為您自己的 Dockerhub ID,並執行 docker login 確保您在執行 Docker 之前已透過身份驗證。

推送之後

示例中的 docker push 會失敗(除非您是 Dockerhub 上 "springio" 組織的一部分)。但是,如果您將配置更改為您自己的 docker ID,它應該會成功。然後您就擁有了一個新的帶標籤的、已部署的映象。

您不必在 Docker 註冊或釋出任何內容即可執行本地構建的 Docker 映象。如果您使用 Docker(透過命令列或 Spring Boot)構建,您仍然有一個本地帶標籤的映象,您可以這樣執行它

$ docker run -p 8080:8080 -t springio/gs-spring-boot-docker
Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=86381K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx450194K (Head Room: 0%, Loaded Class Count: 12837, Thread Count: 250, Total Memory: 1073741824)
....
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)
buildpack 在執行時使用記憶體計算器來調整 JVM 以適應容器大小。

然後可以透過 https://:8080 訪問應用(訪問該地址,它會顯示“Hello Docker World”)。

在使用帶有 boot2docker 的 Mac 時,您通常會在啟動時看到類似以下內容

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——在這種情況下,是虛擬機器的公共 IP https://192.168.59.103:8080

執行時,您可以在容器列表中看到類似以下示例的內容

$ 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

要再次關閉它,可以使用上一個列表中的容器 ID 執行 docker stop(您的 ID 將不同)

docker stop goofy_brown
81c723d22865

如果您願意,也可以在完成後刪除容器(它會保留在檔案系統的某個位置,通常在 /var/lib/docker 下面)

docker rm goofy_brown

使用 Spring Profiles

使用 Spring profiles 執行您新建立的 Docker 映象就像向 Docker run 命令傳遞一個環境變數一樣簡單(對於 prod profile)

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

對於 dev profile,您也可以這樣做

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 訪問容器。

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

總結

恭喜!您已經為 Spring Boot 應用建立了一個 Docker 容器!預設情況下,Spring Boot 應用在容器內部執行在 8080 埠上,我們透過在命令列上使用 -p 將其對映到主機上的同一埠。

另請參閱

以下指南也可能對您有所幫助

想撰寫新指南或貢獻現有指南?請檢視我們的貢獻指南

所有指南的程式碼均採用 ASLv2 許可釋出,文字部分採用 署名-禁止演繹 創意共享許可 釋出。

獲取程式碼