Open10

JavaのDockerイメージ

104104

現在は更新されなくなったイメージ

OpenJDK

https://hub.docker.com/_/openjdk

結論から言うと現在は非推薦イメージとなった模様。
現に上記のDockerHubのページでも非推薦になったことや、早期に代替イメージを探すように促すメッセージが書かれている。

[Docker HubのOpenJDKイメージの利用を更新するためのアドバイス - 赤帽エンジニアブログ]https://rheb.hatenablog.com/entry/updating-docker-hubs-openjdk-image
OpenJDK Java 17 docker image - Stack Overflow

AdoptOpenJDK

https://hub.docker.com/_/adoptopenjdk

こちらもDockerのオフィシャルイメージであるが、eclipse-temurinに代えられて2021年8月から更新されなくなっている。

AdoptOpenJDK は Eclipse Adoptium になる

そもそものAdoptOpenJDKのプロジェクトの運営をAdoptからEclipce Fundationに移管し、プロジェクト名称もAdoptOpenJDKからEclipce Adoptiumプロジェクトになったことが背景にあるっぽい。

104104

2024年2月に候補になるイメージ

  • 複数プロジェクトやディストリビューションが存在するが、用途が決まれば使用するべきイメージも自ずと決まる。(JDKやJREなどを全て突っ込んだイメージとなるとベースイメージの段階で激重になるためだと思われる。
  • マルチステージビルド前提になりつつあるっぽい?

Eclipse temurin

Maven

Javaアプリケーションビルド時におなじみのmavenが含まれるイメージ。
MavenそのものやJavaアプリケーションのビルドに必要なモジュールは含まれているものの、JDKやJREが含まれているわけでないので、ビルド専用イメージで実行には別のJREが必要となる。

新しいバージョンで使うタグとしては以下とか?
maven:3.9-eclipse-temurin-21

Amazon Corretto

ベースはAmazon Linuxという独自のものになっている。
AWS実行向けという認識。

MicrosoftOpenJDK

調査中

Distoress

軽量化を実行しつつ、alpine版を使ったときに問題となるglibcベースに作られている。
シェルが存在しない分、セキュアだと言われている。

Distroless Dockerイメージ(OpenJDK)を試す

参考

JavaのDockerイメージ何選ぶ?

104104

イメージの選び方

ベースイメージに脆弱性がない

私たちがDockerイメージを作成する際は、通常DockerHub上にある何かしらのベースイメージを使う。ベースイメージを使うことによって、ゼロからではなくある程度完成された基盤を用いてDockerイメージを作成できるため便利であるが、ベースイメージに脆弱性がある場合にはそれらも引き継いでしまうため注意が必要。

軽量ベースイメージを使用する。

xxx:lastastのタグがついたイメージは完全なOSをベースに作成している場合が多く、容量が大きくなってしまう可能性があある。追加のバイナリ等必要かどうかを考慮し、slimイメージなどを検討するのが良い。

また、Googleがメンテナンスしているイメージにdistoressというものが存在し、

alpineイメージの特性を知る

軽量イメージを使うとなった場合に、上記のslimイメージのほかにalpineイメージが挙げられることが多い。
ただし、多くのLinuxディストリビューションがglibcというベースにしているのに対して、alpineはmusl libcベースに作られており、これがかえってビルド時間を長くしたり不具合を生むことがある模様。

どういうことかと言うと、まずLinuxOSには何かしらのC言語標準ライブラリが搭載されていることが多く、私たちがアプリケーションをLinuxOSなどで動かすときはこのC言語標準ライブラリがアプリケーションとOSのやり取りを仲介している。

このC言語標準ライブラリについては多くのLinuxにはglibcが用いられているが、alipineイメージのLinuxにはmusl libcというものが用いられている。このmuslというのは軽量化を目的としてglibcとは異なる実装をしている。ただ、多くのLinuxアプリケーションがこのglibcに依存しているため、安易にalpineイメージを選択すると思わぬ挙動が発生することがあるということ。

PythonをAlpineイメージで安易に動かす問題点
仕事でPythonコンテナをデプロイする人向けのDockerfile (1): オールマイティ編

思わぬ挙動を引き起こす例にとしては、上記で触れているPythonのパッケージのうちC拡張ライブラリを使用したものが存在する。PyPI(Pythonのライブラリをホストする場所)ではLinux向けにC拡張ライブラリを含むパッケージをビルドするための規約(manylinux1)があり、この形式に沿ったバイナリ形式のパッケージは多くのLinuxディストリビューションで高速にインストールできる。しかし、AlpineLinuxはglibcと実装が違うためにこの規約とマッチしないため、互換上の問題が生じることがある。

Using Alpine can make Python Docker builds 50× slower

こちらで言われているのは以下の通りだと思われる。

  • 通常のパッケージインストールにおいては必要なソースをダウンロードして、コンパイルしてからインストールを行う。
  • PyPIにはPythonWheelというコンパイル済みバイナリ形式のパッケージがホスティングされている。PythonWheel形式を使えば、前述した必要なソースのダウンロードとコンパイルが短縮されるのでインストールの高速化が図れる。
  • ただしglibcを前提としたPythonWheelはmusl libcの動作を想定していないため、Alpineでは.zipやら.tar.gzなどのビルド前のソースコードを落とし、これをalpine向けにwheel形式にコンパイルしている。なのでインストールにすごく時間がかかるということらしい。

alpineでC言語依存モジュールを pip install すると激重になる話 - Qiita

実行の場合にはJRE、ビルドの場合はJDKが含まれる

先ほど軽量イメージのほうが良いという話をしたが、JavaアプリケーションをビルドするにはJDK、アプリケーションの実行にはJREが必要となる。

まず、Javaはコンパイル言語であるため実行時に元のソースコードは不要でビルド環境によって作成されたアーティファクトのみが必要である。そのため実行時にはJREのみが含まれていれば良い。

開発〜ビルドまではローカルで行って、それを本番環境で実行する際には、JREが含まれるイメージに.jarファイルをコピーするのが基本戦略となる。

https://snyk.io/jp/blog/best-practices-to-build-java-containers-with-docker/

JRE含むベースイメージ、例えばAdoptOpenJDKのベースイメージは500MBほどと結構なサイズがあるが、Jlinkというものを使えばJava実行に必要なカスタムランタイムイメージを作成できるため、容量を削減できるらしい。

DockerコミュニティがメンテしているOpenJDKイメージを使って遭遇した問題
SpringBootのdockerイメージを必要最小限に絞りたい(2019年9月版)

JREはJDKに含まれるものなので、実行環境のみ必要という場合はJREを用意できれば良いが、Dockerで開発環境まで作りたい場合は当然ビルド環境も必要なのでJDKが必要となる。

参考

Javaデベロッパが知るべきDockerセキュリティ5つの大切なこと

104104

Eclipse temurin

https://adoptium.net/temurin/releases/
https://hub.docker.com/_/eclipse-temurin

OpenJDK21 OpenJDK17 OpenJDK11 OpenJDK8
Ubuntu ⚪︎ ⚪︎ ⚪︎ ⚪︎

Amazon Corretto

https://aws.amazon.com/jp/corretto/?filtered-posts.sort-by=item.additionalFields.createdDate&filtered-posts.sort-order=desc

https://hub.docker.com/_/amazoncorretto

OpenJDK21 OpenJDK17 OpenJDK11 OpenJDK8
Amazon Linux ⚪︎ .⚪︎ ⚪︎ ⚪︎

Microsoft OpenJDK

OpenJDK 21 OpenJDK 17 OpenJDK 11 OpenJDK 8
Ubuntu 22.04 21-ubuntu 17-ubuntu 11-ubuntu N/A
CBL Mariner 2.0 21-mariner 17-mariner 11-mariner 8-mariner

https://japan.zdnet.com/article/35174075/

Distoress

https://github.com/GoogleContainerTools/distroless/tree/main/java

OpenJDK 21 OpenJDK 17 OpenJDK 11 OpenJDK 8
Distoress ⚪︎ ⚪︎ N/A N/A
104104

Gradle or Maven + Eclipce-temurinを使う

  • 大前提としてマルチステージビルドありきで考える。ビルド用イメージ(JDK)と実行用イメージ(JRE)の2つを用意して、最終的な成果物はJREの方になるイメージ。
  • SpringBootのアプリケーションをDockerで動かすことを考えたときに、特にサーバー構成なども決まっていないような状況だったら、Eclipce Temurinで良いのではないかと考えている。理由としては以下の点が挙げられる。
    • JDKとJREを両方含んでいる。
    • SpringBootの公式チュートリアルでeclipce-temurinが使われている。
  • ただ、別環境でビルドした.jarファイルを放り込むのでなければ別途ビルドツールが必要であるが、Eclipce Temurinにgradleは入ってなさそう。
    • なのでJDKの方はGradleの公式イメージかMavenの公式イメージを使っても良さげ。
      • gradle
      • maven
        • mavenイメージのタグを見るとamazon correttoかeclipce-temurinの名前を冠しており、これらのいずれかにmavenを乗っけたぐらいの感じかも。
104104

ホットスワップの利用

Spring development on Docker

上記を見る限りはdocker-compose.ymlファイルでビルドと実行のコンテナを分離するとホットスワップができるとのこと。まだ確認はしていない。

docker-compose.yml
version: '3.7'
services:

  builder:
    image: gradle:jdk11
    working_dir: /home/gradle/project
    volumes:
      - ./build:/home/gradle/project/build
      - ./src:/home/gradle/project/src
      - ./build.gradle:/home/gradle/project/build.gradle
    command: gradle build --continuous -x test -x testClasses

  api:
    image: openjdk:13-alpine
    volumes:
    - ./build:/app
    depends_on:
      - builder
    command: java -cp "/app/classes/java/main:/app/dependencies/*:/app/resources/main" com.example.Application
plugins {
    id 'org.springframework.boot' version '2.2.1.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.1.0-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    developmentOnly
    runtimeClasspath {
        extendsFrom developmentOnly
    }
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

task copyLibs(type: Copy) {
    from configurations.runtimeClasspath
    into "${buildDir}/dependencies"
}

build.dependsOn(copyLibs)

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.h2database:h2'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

test {
    useJUnitPlatform()
}
104104

Dockerfileの例

Dockerfile
FROM gradle:jdk21 as builder

COPY ./src /home/source/java
WORKDIR /home/source/java
USER root
RUN chown gradle -R gradle /home/source/java
USER gradle
RUN gradle clean build

FROM eclipse-temurin:21.0.2_13-jre-jammy as runner
WORKDIR /home/application/java
COPY --from=builder "/home/source/java/build/libs/demo-0.0.1-SNAPSHOT.jar" .
EXPOSE 8080
ENTRYPOINT [ "java", "-jar", "/home/application/java/demo-0.0.1-SNAPSHOT.jar"]

https://gist.github.com/liemle3893/025624fc02dbecc0e8fd99a40a4ae94c

上記を参考にして少し改変。ビルド時に生成される.jarのファイル名がdemo-0.0.1-SNAPSHOT.jarになっているが、これはdemoという名前でプロジェクトを作成した場合である。

104104

gradle clean buildの設定

build.gradle
jar {
    archiveFileName = "${project.name}-${project.version}.jar"
}

build.gradleファイルで上記のようにjarディレクティブでarchiveFileNameを指定することで、build/libに出力されるjarファイルの名前を変えることができる。
archiveFileNameの指定がない場合は、デフォルトで上記のようにプロジェクト名とバージョンをハイフンで繋いだものが設定される。

▼参考
https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html#org.gradle.api.tasks.bundling.Jar:archiveName

project.namesettings.gradleで、project.versionbuild.gradle内で設定する。

settings.gradle
rootProject.name = 'demo'
build.gradle
group = 'com.example'
version = '0.0.1-SNAPSHOT'

▼参考
https://stackoverflow.com/questions/17262856/how-to-set-the-project-name-group-version-plus-source-target-compatibility-in