Spring ブートコードをコンテナ化するための 9 つのヒント

Dockerでは、活気に満ちた多様で創造的なコミュニティを非常に誇りに思っています。 時々、私たちはブログでコミュニティからのクールな貢献を特集し、私たちのコミュニティが行っている素晴らしい仕事のいくつかを強調しています。 Dockerで何か素晴らしいことに取り組んでいますか? あなたの貢献をアジート・シン・ライナ(@ajeetraina)に送ってください Docker Community Slack そして、私たちはあなたの作品を特集するかもしれません!

多くの開発者がDockerコンテナを使用して Spring Boot アプリケーションをパッケージ化しています。 VMWareの2021年春の現状 レポートによると、コンテナ化されたSpringアプリを実行している組織の数は、2020年の44%と比較して57%に急増しました。

この大幅な成長を推進しているのは何ですか? Webアプリケーションの起動時間を短縮し、リソース使用量を最適化するという需要がますます高まっているため、開発者の生産性が大幅に向上しています。

Spring Bootアプリのコンテナ化が重要なのはなぜですか?

Spring Boot アプリケーションを Docker コンテナーで実行すると、多くの利点があります。 まず、Dockerの使いやすいCLIベースのワークフローにより、開発者はあらゆるスキルレベルの他の開発者向けにコンテナ化されたSpringアプリケーションを構築、共有、実行できます。 次に、開発者は単一のパッケージからアプリをインストールし、数分で起動して実行できます。 第三に、Spring 開発者は、開発と本番の一貫性を確保しながら、ローカルでコーディングとテストを行うことができます。

Spring Boot アプリケーションのコンテナ化は簡単です。 これを行うには .jar 、 又は .war ファイルを JDK 基本イメージに直接保存し、Docker イメージとしてパッケージ化します。 オンラインには、アプリを効果的にパッケージ化するのに役立つ記事が多数あります。 ただし、Docker イメージの脆弱性、イメージの肥大化、イメージ タグの欠落、ビルド パフォーマンスの低下など、多くの重要な懸念事項は対処されていません。 これらの一般的な懸念事項に取り組みながら、Spring Bootコードをコンテナ化するための9つのヒントを共有します。

シンプルな "Hello World" Spring Boot アプリケーション

無人の問題をよりよく理解するために、サンプルの "Hello World" アプリケーションを作成しましょう。 前回のブログ記事では、 この 初期化済みのプロジェクト をダウンロードして ZIP ファイルを生成することで、"Hello World!" アプリケーションを簡単に構築できることを説明しました。 次に、それを解凍し、次の手順を実行してアプリを実行します。

画像4 2

src/main/java/com/example/dockerapp/ ディレクトリの下で、次の内容でファイルを変更できます DockerappApplication.java


package com.example.dockerapp;
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;

@RestController
@SpringBootApplication
public class DockerappApplication {

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

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

}

The following command takes your compiled code and packages it into a distributable format, like a JAR:

./mvnw package
java -jar target/*.jar

By now, you should be able to access “Hello World” via http://localhost:8080.

In order to Dockerize this app, you’d use a Dockerfile.  A Dockerfile is a text document that contains every instruction a user could call on the command line to assemble a Docker image. A Docker image is composed of a stack of layers, each representing an instruction in our Dockerfile. Each subsequent layer contains changes to its underlying layer.

Typically, developers use the following Dockerfile template to build a Docker image.


FROM eclipse-temurin
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]

The first line defines the base image which is around 457 MB. The  ARG instruction specifies variables that are available to the COPY instruction. The COPY copies the  JAR file from the target/ folder to your Docker image’s root. The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime. Lastly, an ENTRYPOINT lets you configure a container that runs as an executable. It corresponds to your java -jar target/*.jar  command.

You’d build your image using the docker build command, which looks like this:


$ docker build -t spring-boot-docker .
Sending build context to Docker daemon  15.98MB
Step 1/5 : FROM eclipse-temurin
---a3562aa0b991
Step 2/5 : ARG JAR_FILE=target/*.jar
---Running in a8c13e294a66
Removing intermediate container a8c13e294a66
---aa039166d524
Step 3/5 : COPY ${JAR_FILE} app.jar
COPY failed: no source files were specified

One key drawback of our above example is that it isn’t fully containerized. You must first create a JAR file by running the ./mvnw package command on the host system. This requires you to manually install Java, set up the  JAVA_HOME environment variable, and install Maven. In a nutshell, your JDK must reside outside of your Docker container — adding even more complexity into your build environment. There has to be a better way.

1) Automate all the manual steps

We recommend building up the JAR during the build process within your Dockerfile itself. The following RUN instructions trigger a goal that resolves all project dependencies, including plugins, reports, and their dependencies:

FROM eclipse-temurin
WORKDIR /app

COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline

COPY src ./src

CMD ["./mvnw", "spring-boot:run"]

Avoid copying the JAR file manually while writing a Dockerfile

2) Use a specific base image tag, instead of latest

When building Docker images, it’s always recommended to specify useful tags which codify version information, intended destination (prod or test, for instance), stability, or other useful information for deploying your application in different environments. Don’t rely on the automatically-created latest tag. Using latest is unpredictable and may cause unexpected behavior. Every time you pull the latest image, it might contain a new build or untested release that could break your application.

For example, using the eclipse-temurin:latest Docker image as a base image isn’t ideal. Instead, you should use specific tags like eclipse-temurin:17-jdk-jammy , eclipse-temurin:8u332-b09-jre-alpin etc.

Avoid using FROM eclipse-temurin:latest in your Dockerfile

3) Use Eclipse Temurin instead of JDK, if possible

On the OpenJDK Docker Hub page, you’ll find a list of recommended Docker Official Images that you should use while building Java applications. The upstream OpenJDK image no longer provides a JRE, so no official JRE images are produced. The official OpenJDK images just contain “vanilla” builds of the OpenJDK provided by Oracle or the relevant project lead.

One of the most popular official images with a build-worthy JDK is Eclipse Temurin. The Eclipse Temurin project provides code and processes that support the building of runtime binaries and associated technologies. These are high performance, enterprise-caliber, and cross-platform.

FROM eclipse-temurin:17-jdk-jammy

WORKDIR /app

COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline

COPY src ./src

CMD ["./mvnw", "spring-boot:run"]

4) Use a Multi-Stage Build

With multi-stage builds, a Docker build can use one base image for compilation, packaging, and unit tests. Another image holds the runtime of the application. This makes the final image more secure and smaller in size (as it does not contain any development or debugging tools). Multi-stage Docker builds are a great way to ensure your builds are 100% reproducible and as lean as possible. You can create multiple stages within a Dockerfile and control how you build that image.

You can containerize your Spring Boot applications using a multi-layer approach. Each layer may contain different parts of the application such as dependencies, source code, resources, and even snapshot dependencies. Alternatively, you can build any application as a separate image from the final image that contains the runnable application. To better understand this, let’s consider the following Dockerfile:

FROM eclipse-temurin:17-jdk-jammy
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]

Spring Boot uses a “fat JAR” as its default packaging format. When we inspect the fat JAR, we see that the application forms a very small part of the entire JAR. This portion changes most frequently. The remaining portion contains the Spring Framework dependencies. Optimization typically involves isolating the application into a separate layer from the Spring Framework dependencies. You only have to download the dependencies layer — which forms the bulk of the fat JAR — once, plus it’s cached in the host system.

The above Dockerfile assumes that the fat JAR was already built on the command line. You can also do this in Docker using a multi-stage build and copying the results from one image to another. Instead of using the Maven or Gradle plugin, we can also create a layered JAR Docker image with a Dockerfile. While using Docker, we must follow two more steps to extract the layers and copy those into the final image.

In the first stage, we’ll extract the dependencies. In the second stage, we’ll copy the extracted dependencies to the final image:

FROM eclipse-temurin:17-jdk-jammy as builder
WORKDIR /opt/app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY ./src ./src
RUN ./mvnw clean install

FROM eclipse-temurin:17-jre-jammy
WORKDIR /opt/app
EXPOSE 8080
COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
ENTRYPOINT ["java", "-jar", "/opt/app/*.jar" ]

The first image is labeled builder. We use it to run eclipse-temurin:17-jdk-jammy, build the fat JAR, and unpack it.

Notice that this Dockerfile has been split into two stages. The later layers contain the build configuration and the source code for the application, and the earlier layers contain the complete Eclipse JDK image itself. This small optimization also saves us from copying the target directory to a Docker image — even a temporary one used for the build. Our final image is just 277 MB, compared to the first stage build’s 450MB size.

5) Use .dockerignore

To increase build performance, we recommend creating a .dockerignore file in the same directory as your Dockerfile. For this tutorial, your .dockerignore file should contain just one line:

target

This line excludes the target directory, which contains output from Maven, from the Docker build context. There are many good reasons to carefully structure a .dockerignore file, but this simple file is good enough for now. Let’s now explain the build context and why it’s essential . The docker build command builds Docker images from a Dockerfile and a “context.” This context is the set of files located in your specified PATH or URL. The build process can reference any of these files.

Meanwhile, the compilation context is where the developer works. It could be a folder on Mac, Windows or a Linux directory. This directory contains all necessary application components like source code, configuration files, libraries, and plugins. With the .dockerignore file, we can determine which of the following elements like source code, configuration files, libraries, plugins, etc. to exclude while building your new image.

Here’s how your .dockerignore file might look if you choose to exclude the conf, libraries, and plugins directory from your build:

Image1 3

6) Favor Multi-Architecture Docker Images

Your CPU can only run binaries for its native architecture. For example, Docker images built for an x86 system can’t run on an Arm-based system. With Apple fully transitioning to their custom Arm-based silicon, it’s possible that your x86 (Intel or AMD) Docker Image won’t work with Apple’s recent M-series chips. Consequently, we always recommended building multi-arch container images. Below is the mplatform/mquery Docker image that lets you query the multi-platform status of any public image, in any public registry:

docker run --rm mplatform/mquery eclipse-temurin:17-jre-alpine
Image: eclipse-temurin:17-jre-alpine (digest: sha256:ac423a0315c490d3bc1444901d96eea7013e838bcf7cc09978cf84332d7afc76)
* Manifest List: Yes (Image type: application/vnd.docker.distribution.manifest.list.v2+json)
* Supported platforms:
- linux/amd64

We introduced the docker buildx command to help you build multi-architecture images. Buildx is a Docker component that enables many powerful build features with a familiar Docker user experience. All builds executed via Buildx run via the Moby BuildKit builder engine. BuildKit is designed to excel at multi-platform builds, or those not just targeting the user’s local platform. When you invoke a build, you can set the --platform flag to specify the build output’s target platform, (like linux/amd64, linux/arm64, or darwin/amd64):

docker buildx build --platform linux/amd64, linux/arm64 -t spring-helloworld .

 

7) Run as non-root user for security purposes

Running applications with user privileges is safer, since it helps mitigate risks. The same applies to Docker containers. By default, Docker containers and their running apps have root privileges. It’s therefore best to run Docker containers as non-root users. You can do this by adding USER instructions within your Dockerfile. The USER instruction sets the preferred user name (or UID) and optionally the user group (or GID) while running the image — and for any subsequent RUN, CMD, or ENTRYPOINT instructions:

FROM eclipse-temurin:17-jdk-alpine
RUN addgroup demogroup; adduser  --ingroup demogroup --disabled-password demo
USER demo

WORKDIR /app

COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline

COPY src ./src

CMD ["./mvnw", "spring-boot:run"]

8) Fix security vulnerabilities in your Java image

Today’s developers rely on third-party code and applications while building their services. By using external software without care, your code may be more vulnerable. Leveraging trusted images and continually monitoring your containers is essential to combating this.

Docker Scout stands out among available security tools for its ability to provide detailed visibility into dependencies on specific images, along with remediation options integrated into developers’ workflows. It offers advanced analysis of vulnerabilities in dependencies and provides recommendations to quickly fix them, either by searching for updated or patched base images or by identifying the exact location of vulnerabilities in different layers.

Designed to simplify the lives of developers, Docker Scout integrates directly into Docker, allowing you to spend more time developing code while keeping vulnerabilities under control. Additionally, Docker focuses on improving the developer experience by matching CVE to package and SBOM to CVEdb, thereby improving vulnerability detection and resolution without the need for additional scans.

Whenever you download a Docker image (for example ,”jsgiraldoh/spring-helloworld:latest”), Docker Desktop recommends that you look at the summary of known vulnerabilities and image recommendations, such as Log4Shell:

$ docker pull jsgiraldoh/spring-helloworld:latest
latest: Pulling from jsgiraldoh/spring-helloworld
df9b9388f04a: Pull complete
d3277e9a7631: Pull complete
13f2c9707109: Pull complete
28060bb0256d: Pull complete
6c5e62ebc1c8: Pull complete
33c5943bedda: Pull complete
eac578d9fcdc: Pull complete
491863a4fe37: Pull complete
063454f659da: Pull complete
Digest: sha256:194b3574389ea82ee6cca08d9b707c1965c3242b0765f2830c1a511f3a1980e3
Status: Downloaded newer image for jsgiraldoh/spring-helloworld:latest
docker.io/jsgiraldoh/spring-helloworld:latest

What's Next?
  View a summary of image vulnerabilities and recommendations → docker scout quickview jsgiraldoh/spring-helloworld:latest

If you run the command recommended by Docker, you will get the following result:

Screenshot of image in docker scout showing summary of vulnerabilities.

The result of the command is a summary of the vulnerabilities found in each layer of our image. We will also have two additional options that will allow us to observe the CVEs and recommendations to solve these vulnerabilities.

Screenshot of docker scout showing critical and high vulnerabilities associate with analyzed image.
Screenshot of docker scout showing recommended fixes for vulnerabilities in analyzed image.

In addition to using the CLI, it is also possible to access the Docker Scout functionality through Docker Desktop, with the Docker Scout option. To begin, install Docker Desktop 4.25.1 on your Mac, Windows, or Linux machine.

Screenshot of docker scout showing advanced image analysis page.

After selecting the image you want to analyze, you must click on the View packages and CVEs section.

Screenshot of docker scout with view of packages and layers.

9) Use the OpenTelemetry API to measure Java performance

How do Spring Boot developers ensure that their apps are faster and performant? Generally, developers rely on third-party observability tools to measure the performance of their Java applications. Application performance monitoring is essential for all kinds of Java applications, and developers must create top notch user experiences.

Observability isn’t just limited to application performance. With the rise of microservices architectures, the three pillars of observability — metrics, traces, and logs — are front and center. Metrics help developers to understand what’s wrong with the system, while traces help you discover how it’s wrong. Logs tells you why it’s wrong, letting developers dig into particular metrics or traces to holistically understand system behavior.

Observing Java applications requires monitoring your Java VM metrics via JMX, underlying host metrics, and Java app traces. Java developers should monitor, analyze, and diagnose application performance using the Java OpenTelemetry API. OpenTelemetry provides a single set of APIs, libraries, agents, and collector services to capture distributed traces and metrics from your application. Check out this video to learn more.

Conclusion

In this blog post, you saw some of the many ways to optimize your Docker images by carefully crafting your Dockerfile and securing your image by using Snyk Docker Extension Marketplace. If you’d like to go further, check out these bonus resources that cover recommendations and best practices for building secure, production-grade Docker images.

ドッカーブログチートシートv3a 1

フィードバック

「春のブートコードをコンテナ化するための0つのヒント」に関する9つの考え