Spring Boot の Docker Image を小さくする方法

# 要約

- カスタム JRE とマルチステージビルドを利用すれば Spring Boot の Docker Image を削減することができます。
  - 以下が結果の要約になります。公式の AdoptOpenJDK の Docker Image よりも 1/5 以下の容量になっていることがわかります。

| AdoptOpenJDK | AdoptOpenJDK-Alpine | Custom Runtime |
| ------- | -------------- | -------------- |
| 436 MB  | 358 MB         | **85.5 MB**    |



# サンプルコード

- 今回説明するサンプルコードは以下に格納しています。こちらのサンプルコードをもとに以降の説明を行っていきます。
  - [java-container-build-sample (Github)](https://github.com/Takaichi00/java-container-build-sample)



# 手順① jdeps で必要なモジュールを取得

`jdeps` コマンドを利用して、SpringBoot アプリケーションの依存モジュールを調べます。

しかし、SpringBoot の実行可能 jar ファイルを `jdeps` で調べても `java.base` と `java.logging` しか出力されず、この JRE だけでは SpringBoot は起動しません。そこで以下のサイトで紹介されていた Shell Script を用いて依存モジュールを取得します。

- [SpringBootのdockerイメージを必要最小限に絞りたい(2019年9月版)](https://www.m3tech.blog/entry/2019/09/13/110000)

```shell
#!/bin/sh
# jdeps-spring-boot

set -eu

readonly TARGET_JAR=$1
readonly TARGET_VER=$2

# jarを展開するディレクトリ
readonly TMP_DIR="/tmp/app-jar"
mkdir -p ${TMP_DIR}
trap 'rm -rf ${TMP_DIR}' EXIT

# jarを展開
unzip -q "${TARGET_JAR}" -d "${TMP_DIR}"

# 出力
jdeps \
    -classpath \'${TMP_DIR}/BOOT-INF/lib/*:${TMP_DIR}/BOOT-INF/classes:${TMP_DIR}\' \
    --print-module-deps \
    --ignore-missing-deps \
    --module-path ${TMP_DIR}/BOOT-INF/lib/javax.activation-api-1.2.0.jar \
    --recursive \
    --multi-release ${TARGET_VER} \
    -quiet \
    ${TMP_DIR}/org ${TMP_DIR}/BOOT-INF/classes ${TMP_DIR}/BOOT-INF/lib/*.jar
```



Shell Script のファイル名を `get-springboot-module.sh`としたとき、 以下のように実行します。

```shell
./get-springboot-module.sh <SpringBoot の jar> <Java Version>
```

実行例: 

```shell
./get-springboot-module.sh demo-0.0.1-SNAPSHOT.jar 11
```

実行結果に必要なモジュールが出力されます。

```
java.base,java.desktop,java.instrument,java.management.rmi,java.naming,java.prefs,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.unsupported
```



## 注意点

`./get-springboot-module.sh` を実行するときは Java 12 以上のバージョンで実行します。

Java 11 では `javafx.media` を解析する際、存在しないクラスを解析しようとすると NullPointerException が発生するバグがあるためです。これを回避するには Java 12 から追加された `--ignore-missing-deps` を指定する必要があります。

- 参考: [Getting a NPE on Java 11 jdeps tool when scanning Spring Boot .jar files](https://stackoverflow.com/questions/59284163/getting-a-npe-on-java-11-jdeps-tool-when-scanning-spring-boot-jar-files)



# 手順② カスタム JRE を作成

- 以下のような Dockerfile を用意します。
- `jlink --add-modules` には手順①で出力したモジュールを記載します。

```Dockerfile
FROM adoptopenjdk/openjdk11:alpine AS java-build
WORKDIR /jlink
ENV PATH $JAVA_HOME/bin:$PATH
RUN jlink --strip-debug --no-header-files --no-man-pages --compress=2 --module-path $JAVA_HOME \
    --add-modules java.base,java.desktop,java.instrument,java.management.rmi,java.naming,java.prefs,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.unsupported \
    --output jre-min

```





# 手順③ マルチステージビルドを利用してカスタム JRE 上で SpringBoot を起動

- 手順②で用意した Dockerfile を追記していきます。

- alpine linux 上で Java を起動するために必要な設定は以下のサイトを参考に追加しました
  - [Dockerで動かす軽量なJava環境の作成](https://qiita.com/zgmf_mbfp03/items/e86dd1f03f96d4aa5dd5)

```Dockerfile
FROM adoptopenjdk/openjdk11:alpine AS java-build
WORKDIR /jlink
ENV PATH $JAVA_HOME/bin:$PATH
RUN jlink --strip-debug --no-header-files --no-man-pages --compress=2 --module-path $JAVA_HOME \
    --add-modules java.base,java.desktop,java.instrument,java.management.rmi,java.naming,java.prefs,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.unsupported \
    --output jre-min


FROM alpine:3.12.0
USER root

RUN apk --update add --no-cache ca-certificates curl openssl binutils xz \
    && GLIBC_VER="2.28-r0" \
    && ALPINE_GLIBC_REPO="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" \
    && GCC_LIBS_URL="https://archive.archlinux.org/packages/g/gcc-libs/gcc-libs-8.2.1%2B20180831-1-x86_64.pkg.tar.xz" \
    && GCC_LIBS_SHA256=e4b39fb1f5957c5aab5c2ce0c46e03d30426f3b94b9992b009d417ff2d56af4d \
    && ZLIB_URL="https://archive.archlinux.org/packages/z/zlib/zlib-1%3A1.2.9-1-x86_64.pkg.tar.xz" \
    && ZLIB_SHA256=bb0959c08c1735de27abf01440a6f8a17c5c51e61c3b4c707e988c906d3b7f67 \
    && curl -Ls https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub -o /etc/apk/keys/sgerrand.rsa.pub \
    && curl -Ls ${ALPINE_GLIBC_REPO}/${GLIBC_VER}/glibc-${GLIBC_VER}.apk > /tmp/${GLIBC_VER}.apk \
    && apk add /tmp/${GLIBC_VER}.apk \
    && curl -Ls ${GCC_LIBS_URL} -o /tmp/gcc-libs.tar.xz \
    && echo "${GCC_LIBS_SHA256}  /tmp/gcc-libs.tar.xz" | sha256sum -c - \
    && mkdir /tmp/gcc \
    && tar -xf /tmp/gcc-libs.tar.xz -C /tmp/gcc \
    && mv /tmp/gcc/usr/lib/libgcc* /tmp/gcc/usr/lib/libstdc++* /usr/glibc-compat/lib \
    && strip /usr/glibc-compat/lib/libgcc_s.so.* /usr/glibc-compat/lib/libstdc++.so* \
    && curl -Ls ${ZLIB_URL} -o /tmp/libz.tar.xz \
    && echo "${ZLIB_SHA256}  /tmp/libz.tar.xz" | sha256sum -c - \
    && mkdir /tmp/libz \
    && tar -xf /tmp/libz.tar.xz -C /tmp/libz \
    && mv /tmp/libz/usr/lib/libz.so* /usr/glibc-compat/lib \
    && apk del binutils \
    && rm -rf /tmp/${GLIBC_VER}.apk /tmp/gcc /tmp/gcc-libs.tar.xz /tmp/libz /tmp/libz.tar.xz /var/cache/apk/*

COPY --from=java-build /jlink/jre-min /opt/jre-min
COPY ./demo-0.0.1-SNAPSHOT.jar /opt/demo/demo-0.0.1-SNAPSHOT.jar
ENV PATH /opt/jre-min/bin:$PATH

EXPOSE 8080
WORKDIR /
CMD ["java", "-jar", "/opt/demo/demo-0.0.1-SNAPSHOT.jar"]
```



## 実行

- 手順③ で作成した Dockerfile があるディレクトリに SpringBoot の jar ファイルを格納し、以下のコマンドを実行します

```shell
docker build -t demo-official-openjdk-custom-runtime:latest .
```

- image ができたことを確認します

```
docker images

REPOSITORY                             TAG                 IMAGE ID            CREATED             SIZE
demo-official-openjdk-custom-runtime   latest              49049142374f        About an hour ago   85.5MB
```

- 以下のコマンドで起動することを確認します。

```shell
docker run -d -p 8080:8080 --name demo-official-openjdk-custom-runtime demo-official-openjdk-custom-runtime:latest
```



# その他参考文献

- [OpenJDK11のdokcerイメージ(1GB)が大きいのでalpine linux+ jlinkで小さいイメージ(85MB)を作成する](https://qiita.com/h-r-k-matsumoto/items/294eeb838cfd062d75b6)

- [OpenJDKのDockerイメージって、なにがあるんでしたっけ？](https://kazuhira-r.hatenablog.com/entry/2019/02/24/183714)