# 春季啟動Docker
許多人正在使用容器包裝他們的Spring Boot應用程序,而構建容器并不是一件容易的事。 這是Spring Boot應用程序開發人員的指南,容器對于開發人員而言并不總是一個很好的抽象-它們迫使您學習和思考非常低級的問題-但是有時您會被要求創建或使用容器,因此有必要了解這些基本要素。 在這里,我們旨在向您展示一些您需要創建自己的容器時可以做出的選擇。
我們將假定您知道如何創建和構建基本的Spring Boot應用程序。 如果不這樣做,請轉到 [入門指南之一](https://spring.io/guides) ,例如有關構建 [REST服務的指南](https://spring.io/guides/gs/rest-service/) 。 從那里復制代碼,并使用以下一些想法進行練習。
在 上也有一個入門指南 Docker ,這也是一個很好的起點,但是它沒有涵蓋我們在此處所進行的選擇的范圍,也沒有詳細介紹。
## 基本的Dockerfile
Spring Boot應用程序很容易轉換為可執行的JAR文件。 所有的 [入門指南](https://spring.io/guides) 都這樣做,從 下載的每個應用程序 [Spring Initializr](https://start.spring.io) 都將具有一個創建可執行JAR的構建步驟。 有了Maven,您 `./mvnw install` 和Gradle一起你 `./gradlew build`。 然后,在項目的頂層,運行該JAR的基本Dockerfile如下所示:
`Dockerfile`
~~~
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 JAR_FILE=target/*.jar -t myorg/myapp .
~~~
對于Gradle:
~~~
$ docker build --build-arg JAR_FILE=build/libs/*.jar -t myorg/myapp .
~~~
當然,一旦選擇了構建系統,就不需要 `ARG`\-您可以對罐子的位置進行硬編碼。 例如,Maven:
`Dockerfile`
~~~
FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
~~~
然后我們可以簡單地用
~~~
$ docker build -t myorg/myapp .
~~~
并像這樣運行它:
~~~
$ docker run -p 8080:8080 myorg/myapp
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.2.RELEASE)
Nov 06, 2018 2:45:16 PM org.springframework.boot.StartupInfoLogger logStarting
INFO: Starting Application v0.1.0 on b8469cdc9b87 with PID 1 (/app.jar started by root in /)
Nov 06, 2018 2:45:16 PM org.springframework.boot.SpringApplication logStartupProfileInfo
...
~~~
如果要在圖像內部四處瀏覽,可以像這樣打開其中的外殼(基本圖像沒有 `bash`):
~~~
$ docker run -ti --entrypoint /bin/sh myorg/myapp
/ # ls
app.jar dev home media proc run srv tmp var
bin etc lib mnt root sbin sys usr
/ #
~~~
我們在示例中使用的高山基礎容器沒有 bash,所以這是一個 ash殼。 它具有以下特點 bash 但不是所有的。
如果您有一個正在運行的容器并且想窺視它,請使用 `docker exec` 你可以這樣做:
~~~
$ docker run --name myapp -ti --entrypoint /bin/sh myorg/myapp
$ docker exec -ti myapp /bin/sh
/ #
~~~
在哪里 `myapp` 是個 `--name` 傳遞給 `docker run`命令。 如果你不使用 `--name` 然后docker分配一個助記符名稱,您可以從該命令的輸出中抓取該名稱 `docker ps`。 您也可以使用容器的SHA標識符代替名稱,該名稱也可以從以下位置看到 `docker ps`.
### 入口點
的 [exec形式](https://docs.docker.com/engine/reference/builder/#exec-form-entrypoint-example) Dockerfile `ENTRYPOINT`用來避免外殼程序包裝Java進程。 好處是java進程將響應 `KILL`信號發送到容器。 在實踐中,這意味著,例如,如果您 `docker run` 您的圖像在本地,可以通過以下方式停止 `CTRL-C`。 如果命令行太長,您可以將其提取到Shell腳本中,然后 `COPY`在運行之前將其放入映像中。 例子:
`Dockerfile`
~~~
FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY run.sh .
COPY target/*.jar app.jar
ENTRYPOINT ["run.sh"]
~~~
切記使用 `exec java …?` 啟動Java進程(以便它可以處理 `KILL` 信號):
`run.sh`
~~~
#!/bin/sh
exec java -jar /app.jar
~~~
入口點另一個有趣的方面是您是否可以在運行時將環境變量注入到Java進程中。 例如,假設您希望具有在運行時添加java命令行選項的選項。 您可以嘗試執行以下操作:
`Dockerfile`
~~~
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","${JAVA_OPTS}","-jar","/app.jar"]
~~~
和
~~~
$ docker build -t myorg/myapp .
$ docker run -p 9000:9000 -e JAVA_OPTS=-Dserver.port=9000 myorg/myapp
~~~
這將失敗,因為 `${}`替換需要外殼; exec表單不使用外殼程序來啟動進程,因此不會應用選項。 您可以通過將入口點移至腳本(例如 `run.sh`上面的示例),或者通過在入口點顯式創建外殼。 例如:
`Dockerfile`
~~~
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
~~~
然后,您可以使用以下命令啟動該應用
~~~
$ docker run -p 8080:8080 -e "JAVA_OPTS=-Ddebug -Xmx128m" myorg/myapp
...
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.0.RELEASE)
...
2019-10-29 09:12:12.169 DEBUG 1 --- [ main] ConditionEvaluationReportLoggingListener :
============================
CONDITIONS EVALUATION REPORT
============================
...
~~~
(顯示完整的部分 `DEBUG` 用生成的輸出 `-Ddebug` 由Spring Boot提供。)
使用 `ENTRYPOINT`像上面這樣的顯式shell意味著您可以將環境變量傳遞到java命令中,但是到目前為止,您還不能為Spring Boot應用程序提供命令行參數。 此技巧無法在端口9000上運行應用程序:
~~~
$ docker run -p 9000:9000 myorg/myapp --server.port=9000
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.0.RELEASE)
...
2019-10-29 09:20:19.718 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
~~~
它不起作用的原因是因為docker命令( `--server.port=9000` 部分)傳遞到入口點( `sh`),而不是它啟動的Java進程。 要解決此問題,您需要從 `CMD` 到 `ENTRYPOINT`:
`Dockerfile`
~~~
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar ${0} ${@}"]
~~~
~~~
$ docker run -p 9000:9000 myorg/myapp --server.port=9000
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.0.RELEASE)
...
2019-10-29 09:30:19.751 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 9000
~~~
注意使用 `${0}` 對于“命令”(在本例中為第一個程序參數),并且 `${@}`用于“命令參數”(其余程序參數)。 如果您使用腳本作為入口點,則不需要 `${0}` (那是 `/app/run.sh`在上面的示例中)。 例子:
`run.sh`
~~~
#!/bin/sh
exec java ${JAVA_OPTS} -jar /app.jar ${@}
~~~
到目前為止,docker配置非常簡單,并且生成的映像不是很有效。 docker映像只有一個文件系統層,其中包含胖子罐,我們對應用程序代碼所做的每次更改都會更改該層,該層可能為10MB或更大(對于某些應用程序甚至為50MB)。 我們可以通過將JAR分為多個層來改善這一點。
### 較小的圖像
請注意,上面示例中的基本圖像是 `openjdk:8-jdk-alpine`。 這 `alpine` 圖像小于標準 `openjdk`來自 庫映像 [Dockerhub的](https://hub.docker.com/_/openjdk/) 。 尚無Java 11的正式高山圖像(AdoptOpenJDK已有一段時間,但不再出現在其 [Dockerhub頁面上](https://hub.docker.com/r/adoptopenjdk/openjdk11/) )。 您還可以通過使用“ jre”標簽而不是“ jdk”在基本映像中節省大約20MB。 并非所有的應用程序都可以與JRE一起使用(而不是JDK),但是大多數的應用程序都可以,并且確實有些組織會強制執行每個應用程序必須遵循的規則,因為存在濫用某些JDK功能(例如編譯)的風險。
可以使您縮小圖像的另一個技巧是使用 [JLink](https://openjdk.java.net/projects/jigsaw/quick-start#linker) ,它與OpenJDK 11捆綁在一起。JLink允許您從完整JDK中的模塊子集構建自定義JRE分發,因此您不需要JRE或JDK在基本圖像中。 原則上,與使用 `openjdk`官方docker映像。 實際上,您還無法使用 `alpine`帶有JDK 11的基本映像,因此您對基本映像的選擇將受到限制,并且可能會導致最終映像的尺寸更大。 另外,您自己的基本映像中的自定義JRE無法在其他應用程序之間共享,因為它們將需要不同的自定義。 因此,對于所有應用程序來說,它們可能都有較小的映像,但是它們仍需要更長的啟動時間,因為它們無法從緩存JRE層中受益。
最后一點突出了圖像構建者的一個真正重要的關注點:目標不一定總是要構建盡可能小的圖像。 較小的圖像通常是一個好主意,因為它們只需要花費較少的時間就可以上傳和下載,但是前提是它們中的所有層都沒有被緩存。 如今,圖像注冊表非常復雜,通過嘗試巧妙地構建圖像,您很容易失去這些功能的優勢。 如果使用公共基礎層,則圖像的總大小將不再是問題,隨著注冊管理機構和平臺的發展,圖像的總大小可能甚至會減少。 話雖如此,嘗試和優化我們的應用程序映像中的各層仍然是重要且有用的,但是目標始終應該是將變化最快的東西放置在最高層中,并共享盡可能多的較大,較低的層其他應用程序中盡可能多的層。
## 更好的Dockerfile
由于罐子本身的包裝方式,Spring Boot胖子罐子自然具有“層”。 如果我們先拆包,它將已經分為內部和外部依賴關系。 要在Docker構建中一步一步做到這一點,我們需要先打開jar的包裝。 例如(堅持使用Maven,但Gradle版本非常相似):
~~~
$ mkdir target/dependency
$ (cd target/dependency; jar -xf ../*.jar)
$ docker build -t myorg/myapp .
~~~
有了這個 `Dockerfile`
`Dockerfile`
~~~
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"]
~~~
現在共有3層,所有應用程序資源都位于后面的2層中。 如果應用程序依存關系不變,則第一層(來自 `BOOT-INF/lib`)不會更改,因此構建會更快,并且只要基本層已被緩存,容器在運行時的啟動也會隨之改變。
我們使用了硬編碼的主應用程序類 hello.Application。 對于您的應用程序,這可能會有所不同。 您可以用另一個參數化它 ARG如果你想要的話。 您也可以復制Spring Boot的脂肪 JarLauncher 進入映像并使用它來運行應用程序-它可以工作,并且您不需要指定主類,但是啟動時會有點慢。
## 調整
如果您想盡快啟動應用程序(大多數人都這樣做),則可以考慮一些調整。 這里有一些想法:
* 使用 `spring-context-indexer`( [鏈接到文檔](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-scanning-index) )。 對于小型應用程序,它不會增加太多,但對您有所幫助。
* 不要使用 [執行器](https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#production-ready) 如果您負擔不起,請 。
* 使用Spring Boot 2.1和Spring 5.1。
* 修復的位置 [春天啟動配置文件(S)](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-application-property-files) 與 `spring.config.location` (命令行參數或系統屬性等)。
* 關閉JMX-您可能不需要在容器中使用- `spring.jmx.enabled=false`
* 使用以下命令運行JVM `-noverify`。 還考慮 `-XX:TieredStopAtLevel=1` (這將在以后減慢JIT速度,但會節省啟動時間)。
* 使用Java 8的容器內存提示: `-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap`。 對于Java 11,默認情況下是自動的。
您的應用程序在運行時可能不需要完整的CPU,但需要多個CPU才能盡快啟動(至少2、4個更好)。 如果您不介意啟動速度較慢,則可以將CPU的速度降低到4以下。如果您被迫以少于4個CPU的速度啟動,則可能有助于設置 `-Dspring.backgroundpreinitializer.ignore=true`因為它阻止了Spring Boot創建可能無法使用的新線程(適用于Spring Boot 2.1.0及更高版本)。
## 多階段構建
這 `Dockerfile`以上假設胖JAR已在命令行上構建。 您也可以使用多階段構建在Docker中執行此步驟,將結果從一個映像復制到另一個映像。 使用Maven的示例:
`Dockerfile`
~~~
FROM openjdk:8-jdk-alpine as build
WORKDIR /workspace/app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
~~~
第一個圖像標記為“ build”,它用于運行Maven并構建胖罐,然后將其解壓縮。 拆包也可以由Maven或Gradle完成(這是《入門指南》中采用的方法)-確實沒有太大區別,只是必須編輯構建配置并添加插件。
請注意,源代碼已分為4層。 較后的層包含構建配置和應用程序的源代碼,較早的層包含構建系統本身(Maven包裝器)。 這是一個很小的優化,這也意味著我們不必復制 `target`目錄到docker映像,甚至是用于構建的臨時映像。
源代碼更改的每個構建都將變慢,因為必須在第一個構建中重新創建Maven緩存。 `RUN`部分。 但是您擁有一個完全獨立的構建,只要擁有docker,任何人都可以運行它來使您的應用程序運行。 在某些環境中(例如,您需要與不懂Java的人共享代碼),這可能非常有用。
### 實驗功能
Docker 18.06帶有一些 [“實驗性”功能](https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md) ,其中包括一種緩存構建依賴項的方法。 要打開它們,您需要在守護程序中添加一個標志( `dockerd`)以及運行客戶端時的環境變量,然后可以在您的計算機上添加第一行魔術 `Dockerfile`:
`Dockerfile`
~~~
# syntax=docker/dockerfile:experimental
~~~
和 `RUN` 指令然后接受一個新的標志 `--mount`。 這是一個完整的例子:
`Dockerfile`
~~~
# syntax=docker/dockerfile:experimental
FROM openjdk:8-jdk-alpine as build
WORKDIR /workspace/app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN --mount=type=cache,target=/root/.m2 ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
~~~
然后運行它:
~~~
$ DOCKER_BUILDKIT=1 docker build -t myorg/myapp .
...
=> /bin/sh -c ./mvnw install -DskipTests 5.7s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:3defa...
=> => naming to docker.io/myorg/myapp
~~~
使用實驗性功能,您可以在控制臺上獲得不同的輸出,但是可以看到,一旦緩存變熱,Maven構建現在僅需幾秒鐘而不是幾分鐘。
這個的Gradle版本 `Dockerfile` 配置非常相似:
`Dockerfile`
~~~
# syntax=docker/dockerfile:experimental
FROM openjdk:8-jdk-alpine AS build
WORKDIR /workspace/app
COPY . /workspace/app
RUN --mount=type=cache,target=/root/.gradle ./gradlew clean build
RUN mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/build/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
~~~
雖然這些功能處于實驗階段,但打開和關閉buildkit的選項取決于版本。 docker您正在使用的。 檢查您所擁有版本的文檔(上面的示例適用于 docker 18.0.6).
## 安全方面
就像在傳統的VM部署中一樣,不應在具有root權限的情況下運行進程。 相反,映像應包含運行該應用程序的非root用戶。
在一個 `Dockerfile`,這可以通過添加另一層來添加(系統)用戶和組,然后將其設置為當前用戶(而不是默認用戶root)來實現:
`Dockerfile`
~~~
FROM openjdk:8-jdk-alpine
RUN addgroup -S demo && adduser -S demo -G demo
USER demo
...
~~~
如果有人設法突破您的應用程序并在容器內運行系統命令,這將限制他們的功能(最小特權原則)。
一些進一步 Dockerfile 命令僅以root用戶身份運行,因此也許您必須將USER命令進一步向下移動(例如,如果您打算將更多軟件包安裝到僅以root用戶身份運行的容器中)。
其他方法,不使用 Dockerfile,可能會更好一些。 例如,在稍后描述的buildpack方法中,默認情況下,大多數實現將使用非root用戶。
另一個考慮因素是,大多數應用程序在運行時可能不需要完整的JDK,因此一旦我們進行了多階段構建,我們就可以安全地切換到JRE基礎映像。 因此,在上面的多階段構建中,我們可以使用
`Dockerfile`
~~~
FROM openjdk:8-jre-alpine
...
~~~
以獲得最終的可運行圖像。 如上所述,這還節省了映像中的一些空間,這些空間將由運行時不需要的工具占用。
## 構建插件
如果你不想打電話 `docker`直接在您的構建中,有很多針對Maven和Gradle的插件可以為您完成此工作。 這里僅僅是少數。
### Spring Boot插件
使用Spring Boot 2.3,您可以選擇直接使用Spring Boot從Maven或Gradle構建映像。 只要您已經在構建Spring Boot jar文件,您只需要直接調用插件即可。 使用 [Maven](https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/html/#build-image) :
~~~
$ ./mvnw spring-boot:build-image
~~~
并與 [Gradle](https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/html/#build-image)
~~~
$ ./gradlew bootBuildImage
~~~
它使用本地docker守護程序(因此必須安裝),但不需要 `Dockerfile`。 結果是一個名為 `docker.io/<group>/<artifact>:latest`默認。 您可以使用以下方法在Maven中修改圖像名稱:
~~~
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>myorg.demo</name>
</image>
</configuration>
</plugin>
</plugins>
</build>
</project>
~~~
并在Gradle中使用
~~~
bootBuildImage {
imageName = "myorg/demo"
}
~~~
The image is built using [Cloud Native Buildpacks](https://buildpacks.io/), where the default builder is optimized for a Spring Boot application (you can customize it but the defaults are useful). The image is layered efficiently, like in the examples above. It also uses the CF memory calculator to size the JVM at runtime based on the container resources available, so when you run the image you will see the memory calculator reporting its results:
~~~
$ docker run -p 8080:8080 myorg/demo
Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=86557K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx450018K (Head Room: 0%, Loaded Class Count: 12868, Thread Count: 250, Total Memory: 1073741824)
...
~~~
### Spotify Maven插件
在 [Spotify的Maven插件](https://github.com/spotify/dockerfile-maven) 是一個受歡迎的選擇。 它要求應用程序開發人員編寫一個 `Dockerfile` 然后運行 `docker`對您來說,就像您在命令行上一樣。 docker image標簽和其他內容有一些配置選項,但它使您應用程序中的docker知識集中在 `Dockerfile`,很多人都喜歡。
對于真正的基本用法,它無需額外配置即可直接使用:
~~~
$ mvn com.spotify:dockerfile-maven-plugin:build
...
[INFO] Building Docker context /home/dsyer/dev/demo/workspace/myapp
[INFO]
[INFO] Image will be built without a name
[INFO]
...
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.630 s
[INFO] Finished at: 2018-11-06T16:03:16+00:00
[INFO] Final Memory: 26M/595M
[INFO] ------------------------------------------------------------------------
~~~
這將構建一個匿名docker鏡像。 我們可以用 `docker` 現在在命令行上,或使用Maven配置將其設置為 `repository`。 示例(不更改 `pom.xml`):
~~~
$ mvn com.spotify:dockerfile-maven-plugin:build -Ddockerfile.repository=myorg/myapp
~~~
或在 `pom.xml`:
`pom.xml`
~~~
<build>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.8</version>
<configuration>
<repository>myorg/${project.artifactId}</repository>
</configuration>
</plugin>
</plugins>
</build>
~~~
### Palantir Gradle插件
該 [真知晶球搖籃插件](https://github.com/palantir/gradle-docker) 可與一 `Dockerfile` 而且還可以生成一個 `Dockerfile` 為你,然后它運行 `docker` 就像在命令行上運行它一樣。
首先,您需要將插件導入到您的 `build.gradle`:
`build.gradle`
~~~
buildscript {
...
dependencies {
...
classpath('gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0')
}
}
~~~
最后,您應用該插件并調用其任務:
`build.gradle`
~~~
apply plugin: 'com.palantir.docker'
group = 'myorg'
bootJar {
baseName = 'myapp'
version = '0.1.0'
}
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"])
}
~~~
在此示例中,我們選擇在特定位置將Spring Boot胖子罐解包。 `build`目錄,這是docker構建的根目錄。 然后是多層(不是多階段) `Dockerfile` 從上面將工作。
### Spring Boot Maven和Gradle插件
Spring Boot構建插件 [Maven](https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/maven-plugin/reference/html/#build-image) 和 [Gradle的](https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/gradle-plugin/reference/html/#build-image) 可用于創建容器映像。 插件會創建一個OCI圖像(與以下格式創建的格式相同): `docker build`)使用 [Cloud Native Buildpacks](https://buildpacks.io/) 。 你不需要 `Dockerfile` 但是您確實需要一個docker守護程序,該守護程序可以是本地的(使用docker構建時使用的守護程序),也可以通過DOCKER\_HOST環境變量遠程使用。
Maven的示例(不更改 `pom.xml`):
~~~
$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myorg/myapp
~~~
和Gradle一起:
~~~
$ ./gradlew bootBuildImage --imageName=myorg/myapp
~~~
第一次構建可能要花很長時間,因為它必須下載一些容器映像和JDK,但是后續的構建會很快。
如果您運行圖像:
~~~
$ docker run -p 8080:8080 -t myorg/myapp
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
~~~
您會看到它正常啟動。 您可能還會注意到,JVM內存需求是在容器內部計算并設置為命令行選項的。 這與Cloud Foundry構建包中使用多年的內存計算相同。 它代表了對各種JVM應用程序(包括但不限于Spring Boot應用程序)的最佳選擇的重大研究,其結果通常比JVM的默認設置要好得多。 您可以自定義命令行選項,并使用環境變量覆蓋內存計算器。
### Jib Maven和Gradle插件
Google有一個名為 開源工具 [Jib的](https://github.com/GoogleContainerTools/jib) ,它相對較新,但出于多種原因卻很有趣。 可能最有趣的是,您不需要docker來運行它-它使用與您獲得的相同的標準輸出來構建映像 `docker build` 但不使用 `docker`除非您要求-否則它可以在未安裝docker的環境中運行(在構建服務器中并不罕見)。 您也不需要 `Dockerfile` (無論如何都會被忽略),或者您的任何內容 `pom.xml` 以獲得在Maven中構建的圖像(Gradle要求您至少在以下位置安裝插件: `build.gradle`).
Jib的另一個有趣特征是,它對層有看法,并且以與多層略有不同的方式對層進行了優化。 `Dockerfile`在上面創建。 就像在胖子罐中一樣,Jib將本地應用程序資源與依賴項分離開來,但它走得更遠,而且還將快照依賴項放入一個單獨的層中,因為它們更容易發生變化。 有一些配置選項可用于進一步自定義布局。
Maven的示例(不更改 `pom.xml`):
~~~
$ mvn com.google.cloud.tools:jib-maven-plugin:build -Dimage=myorg/myapp
~~~
要運行以上命令,您將需要具有在以下位置推送到Dockerhub的權限: `myorg`存儲庫前綴。 如果您已通過 `docker` 在命令行上,它將在您本地 `~/.docker`配置。 您還可以在您的服務器中設置Maven“服務器”身份驗證 `~/.m2/settings.xml` (這 `id` 存儲庫的數量很重要):
`settings.xml`
~~~
<server>
<id>registry.hub.docker.com</id>
<username>myorg</username>
<password>...</password>
</server>
~~~
還有其他選項,例如,您可以針對docker守護程序在本地進行構建(例如運行 `docker` 在命令行上),使用 `dockerBuild` 目標而不是 `build`。 還支持其他容器注冊表,對于每個容器注冊表,您將需要通過docker或Maven設置來設置本地身份驗證。
一旦您將gradle插件包含在其中,它便具有類似的功能 `build.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')
}
}
~~~
然后您可以用
~~~
$ ./gradlew jib --image=myorg/myapp
~~~
與Maven構建一樣,如果您已通過 `docker` 在命令行上,圖像推送將從您本地的身份驗證 `~/.docker` 配置。
## 持續集成
如今(或應該如此),自動化已成為每個應用程序生命周期的一部分。 人們用來實現自動化的工具往往非常擅長于從源代碼調用構建系統。 因此,如果您得到一個docker映像,并且構建代理中的環境與開發人員自己的環境充分匹配,這可能就足夠了。 向Docker注冊表進行身份驗證可能是最大的挑戰,但是所有自動化工具中都有一些功能可以幫助實現這一點。
但是,有時最好將容器創建完全留給自動化層,在這種情況下,可能不需要污染用戶的代碼。 容器創建很棘手,開發人員有時并不真正在意它。 如果用戶代碼更簡潔,則其他工具更有可能“做正確的事”,應用安全修復程序,優化緩存等。自動化有多種選擇,并且這些天都將具有與容器相關的某些功能。 我們只看幾個。
### 大堂
[Concourse](https://concourse-ci.org) 是可用于CI和CD的基于管道的自動化平臺。 它在Pivotal內部大量使用,該項目的主要作者在那里工作。 除CLI外,Concourse中的所有內容都是無狀態的,并且所有內容都在容器中運行。 由于運行容器是自動化管道的主要業務順序,因此很好地支持創建容器。 該 [泊塢窗圖像資源](https://github.com/concourse/docker-image-resource) 負責保持你構建的輸出狀態更新,如果它是一個容器圖像。
這是為上面的示例構建docker映像的示例管道,假設它位于github上 `myorg/myapp` 并有一個 `Dockerfile` 在根處,并在其中創建任務 `src/main/ci/build.yml`:
~~~
resources:
- name: myapp
type: git
source:
uri: https://github.com/myorg/myapp.git
- name: myapp-image
type: docker-image
source:
email: {{docker-hub-email}}
username: {{docker-hub-username}}
password: {{docker-hub-password}}
repository: myorg/myapp
jobs:
- name: main
plan:
- task: build
file: myapp/src/main/ci/build.yml
- put: myapp-image
params:
build: myapp
~~~
管道的結構非常具有聲明性:您可以定義“資源”(輸入或輸出或兩者兼有)和“作業”(使用并向資源應用操作)。 如果任何輸入資源發生更改,則會觸發新的構建。 如果作業期間任何輸出資源發生更改,則將對其進行更新。
可以在與應用程序源代碼不同的位置定義管道。 對于一般的構建設置,任務聲明也可以集中或外部化。 如果這是滾動的方式,則可以將開發和自動化之間的關注點分離開。
### 詹金斯
[Jenkins](https://jenkins.io) 是另一種流行的自動化服務器。 它具有廣泛的功能,但是,與此處的其他自動化示例最接近的 是 [管道](https://jenkins.io/doc/book/pipeline/docker/) 功能 功能。 這是一個 `Jenkinsfile` 它使用Maven構建Spring Boot項目,然后使用 `Dockerfile` 構建圖像并將其推送到存儲庫:
`Jenkinsfile`
~~~
node {
checkout scm
sh './mvnw -B -DskipTests clean package'
docker.build("myorg/myapp").push()
}
~~~
對于需要在構建服務器中進行身份驗證的(現實的)泊塢庫,您可以將憑證添加到 `docker` 上面使用的對象 `docker.withCredentials(…?)`.
## 構建包
Spring Boot Maven和Gradle插件使用buildpack的方式與 packCLI在以下示例中進行操作。 主要區別在于插件使用 docker 運行構建,而 pack不需要。 給定相同的輸入,得到的圖像是相同的。
[Cloud Foundry](https://www.cloudfoundry.org/) 多年來一直在內部使用容器,用于將用戶代碼轉換為容器的技術的一部分是Build Packs,該思想最初是從 借來的 [Heroku](https://www.heroku.com/) 。 當前的buildpacks(v2)生成通用二進制輸出,該輸出由平臺組裝到容器中。 在 [新一代buildpacks的](https://buildpacks.io/) (V3)是的Heroku和其他公司,包括樞紐之間的合作,并直接和明確地構建容器的圖像。 這對于開發人員和操作員而言非常有趣。 開發人員不需要太在乎如何構建容器的細節,但是如果需要,他們可以輕松地創建一個容器。 Buildpacks還具有許多用于緩存構建結果和依賴項的功能,因此,與本地Docker構建相比,Buildpack的運行速度通常要快得多。 操作員可以掃描容器以審核其內容,并對其進行轉換以修補它們以進行安全更新。 您可以在本地運行構建包(例如,在開發人員機器上或在CI服務中),也可以在Cloud Foundry之類的平臺上運行。
buildpack生命周期的輸出是一個容器映像,但是您不需要docker或 `Dockerfile`,因此對CI和自動化友好。 輸出映像中的文件系統層由buildpack進行控制,通常,將進行許多優化,而無需開發人員知道或關心它們。 還有一個 [應用程序二進制接口](https://en.wikipedia.org/wiki/Application_binary_interface) 在較低層(如包含操作系統的基礎映像)與較高層(如包含中間件和特定于語言的依賴關系)之間 。 如果存在安全更新,那么諸如Cloud Foundry之類的平臺就可以修補較低的層,而不會影響應用程序的完整性和功能。
為了讓您了解buildpack的功能,這里是一個 使用 的示例 [Pack CLI](https://github.com/buildpack/pack) 從命令行 (它將與我們在本指南中使用的示例應用程序一起使用,不需要 `Dockerfile` 或任何特殊的構建配置):
~~~
$ pack build myorg/myapp --builder=cloudfoundry/cnb:bionic --path=.
2018/11/07 09:54:48 Pulling builder image 'cloudfoundry/cnb:bionic' (use --no-pull flag to skip this step)
2018/11/07 09:54:49 Selected run image 'packs/run' from stack 'io.buildpacks.stacks.bionic'
2018/11/07 09:54:49 Pulling run image 'packs/run' (use --no-pull flag to skip this step)
*** DETECTING:
2018/11/07 09:54:52 Group: Cloud Foundry OpenJDK Buildpack: pass | Cloud Foundry Build System Buildpack: pass | Cloud Foundry JVM Application Buildpack: pass
*** ANALYZING: Reading information from previous image for possible re-use
*** BUILDING:
-----> Cloud Foundry OpenJDK Buildpack 1.0.0-BUILD-SNAPSHOT
-----> OpenJDK JDK 1.8.192: Reusing cached dependency
-----> OpenJDK JRE 1.8.192: Reusing cached launch layer
-----> Cloud Foundry Build System Buildpack 1.0.0-BUILD-SNAPSHOT
-----> Using Maven wrapper
Linking Maven Cache to /home/pack/.m2
-----> Building application
Running /workspace/app/mvnw -Dmaven.test.skip=true package
...
---> Running in e6c4a94240c2
---> 4f3a96a4f38c
---> 4f3a96a4f38c
Successfully built 4f3a96a4f38c
Successfully tagged myorg/myapp:latest
$ docker run -p 8080:8080 myorg/myapp
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.0.RELEASE)
2018-11-07 09:41:06.390 INFO 1 --- [main] hello.Application: Starting Application on 1989fb9a00a4 with PID 1 (/workspace/app/BOOT-INF/classes started by pack in /workspace/app)
...
~~~
這 `--builder`是運行buildpack生命周期的docker映像-通常,它將是所有開發人員或單個平臺上所有開發人員的共享資源。 您可以在命令行上設置默認構建器(在 `~/.pack`),然后從后續版本中忽略該標志。
這 cloudfoundry/cnb:bionic 生成器還知道如何從可執行的jar文件生成映像,因此您可以使用 mvnw 首先,然后指向 --path 到jar文件以得到相同的結果。
## 基尼特語
容器和平臺領域的另一個新項目是 [Knative](https://cloud.google.com/knative/) 。 Knative有很多東西,但是如果您不熟悉Knative,則可以將其視為構建無服務器平臺的基礎。 它基于 構建, [Kubernetes](https://kubernetes.io) 因此最終它會使用容器映像,并將它們轉換為平臺上的應用程序或“服務”。 但是,它的主要功能之一是能夠使用源代碼并為您構建容器,從而使其對開發人員和操作員更友好。 [Knative Build](https://github.com/knative/build) 是執行此操作的組件,它本身是一個將用戶代碼轉換為容器的靈活平臺-您幾乎可以按照自己喜歡的任何方式進行操作。 一些模板提供了常見的模式,例如Maven和Gradle構建,以及使用 多階段 構建 [Kaniko的 docker](https://github.com/GoogleContainerTools/kaniko) 。 還有一個使用 的模板, [Buildpacks](https://github.com/knative/build-templates/tree/master/buildpack) 這對我們來說很有趣,因為buildpacks一直對Spring Boot都有很好的支持。
## 閉幕
本指南介紹了許多用于為Spring Boot應用程序構建容器映像的選項。 所有這些都是完全有效的選擇,現在由您決定需要哪一個。 您的第一個問題應該是“我真的需要構建容器映像嗎?” 如果答案是“是”,那么您的選擇可能會受到效率和可緩存性以及關注點分離的影響。 您是否希望使開發人員不必過多地了解如何創建容器映像? 您是否想讓需要修補操作系統和中間件漏洞的開發人員負責更新映像? 也許開發人員需要對整個過程進行完全控制,并且他們擁有所需的所有工具和知識。
- springboot概述
- springboot構建restful服務
- spring構建一個RESTful Web服務
- spring定時任務
- 消費RESTful Web服務
- gradle構建項目
- maven構建項目
- springboot使用jdbc
- springboot應用上傳文件
- 使用LDNA驗證用戶
- 使用 spring data redis
- 使用 spring RabbitTemplate消息隊列
- 用no4j訪問nosql數據庫
- springboot驗證web表單
- Spring Boot Actuator構j建服務
- 使用jms傳遞消息
- springboot創建批處理服務
- spring security保護web 安全
- 在Pivotal GemFire中訪問數據
- 使用Spring Integration
- 使用springboot jpa進行數據庫操作
- 數據庫事務操作
- 操作mongodb
- springmvc+tymleaf創建web應用
- 將Spring Boot JAR應用程序轉換為WAR
- 創建異步服務
- spring提交表單
- 使用WebSocket構建交互式Web應用程序
- 使用REST訪問Neo4j數據
- jquery消費restful
- springboot跨域請求
- 消費SOAP Web服務
- springboot使用緩存
- 使用Vaadin創建CRUD UI
- 使用REST訪問JPA數據
- 使用REST訪問Pivotal GemFire中的數據
- 構建soap服務
- 使用rest訪問mongodb數據
- 構建springboot應用docker鏡像
- 從STS部署到Cloud Foundry
- springboot測試web應用
- springboot訪問mysql
- springboot編寫自定義模塊并使用
- 使用Google Cloud Pub / Sub進行消息傳遞
- 構建反應式RESTful Web服務
- 使用Redis主動訪問數據
- Spring Boot 部署到Kubernetes
- 使用反應式協議R2DBC訪問數據
- Spring Security架構
- spring構建Docker鏡像詳解
- Spring Boot和OAuth2
- springboot應用部署到k8s
- spring構建rest服務詳解