SpringBootアプリケーションのDockerイメージを作る!

SpringBootはおしごとで使うのにDockerイメージ作ったことないなと思って、やっておこうかと。

Google先生に聞いたらSpring 公式ブログがDockerイメージの作り方について書いてました。2つほど見つけましたが、下の記事のほうが詳しく書かれています。このあたりを参考にDockerイメージを作ってみます。

最終的に出来上がったものはGitHubに載せました。(Springのサンプルを今後も作ると思うので、Gradleマルチプロジェクト構成にしています。)

プロジェクト構成

プロジェクトの構成は以下です。

.
├── README.md
├── build.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
├── simple-spring-boot-docker
│   ├── Dockerfile
│   ├── README.md
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── example
│           │           └── spring
│           │               └── docker
│           │                   └── Application.java
│           └── resources
└── spring-boot-docker-multi-stage-build
    ├── Dockerfile
    ├── README.md
    └── src
        └── main
            └── java
                └── com
                    └── example
                        └── spring
                            └── docker
                                └── Application.java

simple-spring-boot-docker がビルドしたJarをコピーしてDockerイメージを作る例。spring-boot-docker-multi-stage-build はDockerマルチステージビルドを使って、Jarを作るステップを含んだDockerfileを書いています。

Gradleのマルチプロジェクトの構成をとっているので、ルートプロジェクトの build.gradle.ktsプラグインや依存関係を定義してます。 subproject ブロックを使うことでサブプロジェクトの設定が一箇所で行なえます。プラグインは以下のようにルートプロジェクトで定義したあと、サブプロジェクトごとに apply で適用していく必要があるようです。

spring-examples/build.gradle.kts at master · BooookStore/spring-examples · GitHub

plugins {
    id("org.springframework.boot") version "2.3.1.RELEASE" apply false
    id("io.spring.dependency-management") version "1.0.9.RELEASE" apply false
    id("java")
}

subprojects {
    apply(plugin = "org.springframework.boot")
    apply(plugin = "io.spring.dependency-management")
    apply(plugin = "java")

    java {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation("org.springframework.boot:spring-boot-starter-web")
    }
}

Dockerイメージを作ってみる

ビルドしたJarをコピーしてDockerイメージを作るのは非常にシンプルです。以下がそのDockerfileですが COPY を使ってJarをコピーしたあと、 ENTRYPOINT でアプリケーションを起動するだけです。

spring-examples/Dockerfile at master · BooookStore/spring-examples · GitHub

FROM openjdk:11-jre-slim
WORKDIR /app
COPY ./build/libs/simple-spring-boot-docker.jar .
ENTRYPOINT ["java", "-jar", "simple-spring-boot-docker.jar"]

マルチステージビルドを使う場合はちょっと複雑な印象です。Dockerfileが以下になります。

spring-examples/Dockerfile at master · BooookStore/spring-examples · GitHub

# Build Application
FROM openjdk:11-jdk-slim AS builder
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts build.gradle.kts
COPY settings.gradle.kts settings.gradle.kts
COPY spring-boot-docker-multi-stage-build spring-boot-docker-multi-stage-build
RUN ["./gradlew", "spring-boot-docker-multi-stage-build:build"]

# Build Docker Image
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /app/spring-boot-docker-multi-stage-build/build/libs/spring-boot-docker-multi-stage-build.jar .
ENTRYPOINT ["java", "-jar", "spring-boot-docker-multi-stage-build.jar"]

ステップが2つに分かれています。最初にビルドに必要なリソースをコピーしてJarを作ります。次に最終的なイメージを作ります。

マルチステージビルドを利用するメリットとしてはビルドする環境も含めて統一化できる点かなと思います。こっちのPCだと動くという状況をなくすためにはいいと思いますね。(CI環境とかがあるのであればそちらで担保できますけどね)

このDockerfileを実行するときに知ったのですが、Dockerfileが置いてあるディレクトリの外のファイルをコピーすることはできないみたいです。コピーしようとするとこんなエラーが出てしまいます。

COPY failed: Forbidden path outside the build context: ../ ()

このような場合、docker build コマンドを叩くディレクトリをコピーしたいファイルよりも上位のディレクトリにすればOKです。その場合はDockerfileは docker build コマンドを叩くディレクトリからの相対パスを記述する必要があります。また、コマンドの引数にファイルを指定する必要もあります。