Kotlin、Spring Boot、Gradle、MySQLのアプリをDockerコンテナ化

この記事では、Kotlin、Spring Boot、Gradle、MySQLのアプリをDockerコンテナ化する方法を解説します。

初心者も理解できるように、
1. プロジェクト作成
1. DockerでMySQLコンテナ作成
1. Spring Bootアプリのコンテナ作成
1. 2と3のコンテナ連携

までの手順をステップバイステップで説明します。

完成モジュールのGithubリポジトリも公開しているので、参考にしてください。

https://github.com/taichi-web-engineer/docker-spring-mysql-app

# 目次
- [利用PC](#利用pc)
- [Spring Initializrでプロジェクト作成](#spring-initializrでプロジェクト作成)
  - [設定の補足](#設定の補足)
  - [追加するDependencies](#追加するdependencies)
- [プロジェクト全体のモジュール作成](#プロジェクト全体のモジュール作成)
- [DockerでMySQL起動](#dockerでmysql起動)
  - [設定の補足](#設定の補足-1)
- [IntelliJでMySQLに接続](#intellijでmysqlに接続)
- [Spring BootでMySQLのデータを画面表示するための設定](#spring-bootでmysqlのデータを画面表示するための設定)
  - [設定の補足](#設定の補足-2)
- [Spring BootのDockerコンテナ化](#spring-bootのdockerコンテナ化)
- [DockerでSpring BootとMySQLの連携](#dockerでspring-bootとmysqlの連携)
# 利用PC
使っているPCはIntel CPUのMacでOSはmacOS Montereyです。
# Spring Initializrでプロジェクト作成
[Spring Initializr](https://start.spring.io/)でSpring Bootプロジェクトを作成します。

![Spring Initializrの設定内容](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993684/5ed4963e-c22e-5f22-dff0-1471c1407897.png)

各設定は上記の画像と同じように入力します。
* Project：Gradle - Kotlin
* Language：Kotlin
* Spring Boot：3.2.5
* Group：com.test-project
* Artifact：backend
* Name：backend
* Description：DockerでKotlin、Spring BootとMySQL連携デモプロジェクト
* Package name：com.test-project.backend
* Packaging：Jar
* Java：21
## 設定の補足
* Spring Boot：より新しいLTSバージョンがあれば、3.2.5以上でもOK
* Group：Groupにはアプリの逆ドメインを指定するのが慣例。例えばgithub.comなら`com.github`。Groupの設定値はパッケージ名に使われる
* Artifact、Name：Spring Bootプロジェクトのルートディレクトリ名、パッケージ名、ビルド時のjarファイル名、設定ファイルなどに使われる
* Java：[最新のLTSバージョン](https://www.oracle.com/jp/java/technologies/downloads/)を選択
## 追加するDependencies
* MySQL Driver：Spring BootからMySQLへ接続するためのDriver
* Spring Web：Spring BootでREST APIを作成するためのライブラリ
* Spring Data JPA：Spring BootでSQLを扱いやすくするためのライブラリ
<br>

設定ができたら`GENERATE`ボタンでモジュールをダウンロードしてください。入力が面倒な場合は[設定済リンク](https://start.spring.io/#!type=gradle-project-kotlin&language=kotlin&platformVersion=3.2.5&packaging=jar&jvmVersion=21&groupId=com.test-project&artifactId=backend&name=backend&description=Docker%E3%81%A7Kotlin%E3%80%81Spring%20Boot%E3%81%A8MySQL%E9%80%A3%E6%90%BA%E3%83%87%E3%83%A2%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88&packageName=com.test-project.backend&dependencies=mysql,web,data-jpa)からダウンロードも可能です。
# プロジェクト全体のモジュール作成
```
docker-spring-mysql-app/
├── backend/
├── db/
└── compose.yaml
```
まず上記のディレクトリ、ファイルを作成します。

各ディレクトリの中身は
* backend：Spring Initializrでダウンロードしたモジュール
* db：MySQLのDockerコンテナに必要なファイル

です。詳細は後で説明します。

`docker-spring-mysql-app/compose.yaml`でSpring BootとMySQLのDockerコンテナを連携させる構成です。
# DockerでMySQL起動
MacでDockerを使うため、公式サイトにそってDocker Desktopをインストールしておきます。

https://matsuand.github.io/docs.docker.jp.onthefly/desktop/mac/install/

MySQLのDockerコンテナを作成するため、`docker-spring-mysql-app/compose.yaml`に以下を入力します。
```yaml:app/compose.yaml
services:
  db:
    image: mysql:8.1.0
    restart: always
    healthcheck:
      test:
        [
          'CMD-SHELL',
          'mysqladmin ping -h 127.0.0.1 --password="$$(cat /run/secrets/db-password)" --silent',
        ]
      interval: 3s
      retries: 5
      start_period: 30s
    secrets:
      - db-password
    volumes:
      - db-data:/var/lib/mysql
      - ./db/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
    ports:
      - '3307:3306' # ホストの3306が既に使用されているため3307にマッピング
    environment:
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password
      - TZ=Asia/Tokyo
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  db-data:

secrets:
  db-password:
    file: db/password.txt
```
上記設定はDocker公式が出しているawsome-composeの[nginx-golang-mysql](https://github.com/docker/awesome-compose/tree/master/nginx-golang-mysql)リポジトリを参考にしています。
## 設定の補足
* image: mysql:8.1.0：データベースのバージョン管理に[Flyway](https://flywaydb.org/)を使うため、[Flywayで扱えるMySQL最新バージョン](https://documentation.red-gate.com/fd/mysql-184127601.html)の8.1を使う
* restart：alwaysでDocker再起動時、コンテナ停止、クラッシュ時にコンテナを常に再起動する
* healthcheck：dbの稼働チェック。healthcheck結果を使って後ほどSpring Bootのコンテナと連携
* secrets：db-passwordで定義しているファイルの内容がコンテナの/run/secrets/db-passwordにファイルとしてマウントされる
* volumes：MySQLのデータ永続化と、コンテナ起動時にinit-db.sqlを実行する
* ports：ホストの3307ポートをコンテナの3306ポートにマッピング
* environment：MySQLで初期作成するデータベース名とルートパスワード、タイムゾーンを設定
* logging：logファイルを10MB×3ファイルまでしか作成しない。3ファイルを超えると最古のファイルが削除される
## .envの作成
`docker-spring-mysql-app/.env`を作成し、`compose.yaml`の環境変数`${MYSQL_DATABASE}`を定義します。
```ini:.env
MYSQL_DATABASE=test_db
```
## password.txtの作成
`docker-spring-mysql-app/db/password.txt`でMySQLのルートパスワードを定義します。
```password.txt
testPassword
```
## init-db.sqlの作成
`docker-spring-mysql-app/db/init-db.sql`でコンテナ初期起動時にSpring Bootから使うユーザ作成をし、権限を与えます。
```sql:init-db.sql
create user 'app_user'@'%' identified by 'appUserPassword';
grant select,insert,update,delete on test_db.* to 'app_user'@'%';
```
## .gitignoreの作成
`docker-spring-mysql-app/.gitignore`を作成し、パスワードなどが設定されたファイルをgitの管理対象から外します。
```.gitignore
.env
password.txt
init-db.sql
```
<br>

ここまでできたら、DockerでMySQLのコンテナを起動してみます。

Docker Desktopを起動し、ターミナルで`docker-spring-mysql-app`ディレクトリに移動して`docker compose up -d`を実行しましょう。Docker DesktopのContainersメニューでMySQLのコンテナの起動が確認できるはずです。

![MySQLコンテナが表示されたDocker Desktop](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993684/660192ab-71ca-51bf-b427-c79d9b8cfbb0.png)
# IntelliJでMySQLに接続
IntelliJの`ファイル` > `開く`で`docker-spring-mysql-app`ディレクトリを開きます。

画面右のデータベースアイコンからメニューを開き`+` > `データソース` > `MySQL` > `MySQL`をクリック。

![IntelliJでMySQLの接続設定画面の開き方](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993684/15054255-5052-231d-7504-c21bafb1b2e4.png)

* 名前：任意の値
* ホスト：localhost
* ポート：3307
* ユーザー：root
* パスワード：testPassword
* データベース：test_db

を入力して接続のテストをクリックし、成功するか確かめましょう。

![MySQL接続テスト成功の画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993684/195c8cb5-4c5b-683f-3ed7-c0d0dbd4738b.png)

成功したら適用とOKをクリック。データベースメニューから`test_db`と`app_user`が作成されていることが確認できます。

![MySQLでtest_dbとapp_userが表示された画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993684/368cff2a-e8b1-4fd4-d761-fa18d95edfea.png)

Spring BootとMySQLの連携時に使うためのテーブルとデータを作成しておきます。
```sql
CREATE TABLE test_table (id int unsigned AUTO_INCREMENT primary key , value varchar(50));
INSERT INTO test_table (value) VALUES ('テスト');
```
<br>

IntelliJのクエリコンソールから上記SQLを実行しても良いですが、データベースバージョン管理のために[Flyway](https://flywaydb.org/)を使うのがおすすめです。

IntelliJでFlywayを設定する方法は以下の記事を参照してください。

(後日公開予定)
# Spring BootでMySQLのデータを画面表示するための設定
Spring Boot実行時にMySQLのデータを画面に表示するための設定をします。

`docker-spring-mysql-app/backend/src/main/resources/application.properties`をより可読性の高い`application.yaml`にリネームし、以下設定を入力します。
```yaml:application.yaml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: ${DATABASE_URL}
    username: ${APP_DATABASE_USER}
    password: ${APP_DATABASE_PASSWORD}
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        jdbc:
          time_zone: Asia/Tokyo
        format_sql: true
```
## 設定の補足
* show-sql: true：実行されるSQLがコンソール表示されるようになる
* ddl-auto: none：jpaがスキーマの自動生成、更新をしない
* format_sql: true：コンソール表示されるSQLが整形される
## .envの追記
`docker-spring-mysql-app/.env`環境変数ファイルに`application.yaml`で使う環境変数を追記します。
```ini:.env
MYSQL_DATABASE=test_db
DATABASE_URL=jdbc:mysql://db:3306/test_db
APP_DATABASE_USER=app_user
APP_DATABASE_PASSWORD=appUserPassword
```
## MySQLデータを画面表示するための設定
MySQLデータを画面表示するためのコードを書いていきます。

`docker-spring-mysql-app/backend/src/main/kotlin`の`com.testproject.backend`パッケージに`entry`パッケージを作成し、その中に`TestTable.kt`を作成。
```kotlin:TestTable.kt
package com.testproject.backend.entity

import jakarta.persistence.*

@Entity
@Table(name = "test_table")
class TestTable(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    val id: Int,

    @Column(length = 50)
    val value: String?
)

```
同じく`com.testproject.backend`パッケージに`repository`パッケージを作成し、その中に`TestRepository.kt`を作成。
```kotlin:TestRepository.kt
package com.testproject.backend.repository

import com.testproject.backend.entity.TestTable
import org.springframework.data.repository.CrudRepository

interface TestRepository : CrudRepository<TestTable, Int>

```
そして`com.testproject.backend`パッケージに`controller`パッケージを作成し、その中に`TestController.kt`を作成。
```kotlin:TestController.kt
package com.testproject.backend.controller

import com.testproject.backend.repository.TestRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class TestController {
    @Autowired
    lateinit var testRepository: TestRepository

    @GetMapping("/")
    fun index(): String {
        val testTable = testRepository.findById(1)

        return testTable.get().value!!
    }
}

```
これでSpring Bootアプリを起動した状態で`localHost:8080`にアクセスすると`テスト`が表示されるよう設定できました。
# Spring BootのDockerコンテナ化
作成したSpring BootアプリをDockerコンテナ上で起動できるようにします。

`docker-spring-mysql-app/backend/Dockerfile`を作成しましょう。
```dockerfile
# ステップ 1: ビルドステージ
FROM gradle:8.3.0-jdk17 AS build

# 作業ディレクトリを設定
WORKDIR /app

# 必要なファイルのみコピーしてDockerキャッシュを活用
COPY build.gradle.kts settings.gradle.kts /app/
COPY gradle /app/gradle

# 依存関係をダウンロード（Dockerキャッシュを活用）
RUN gradle build -x test --parallel --no-daemon || return 0

# 残りのソースコードをコピーしてアプリケーションをビルド
COPY src /app/src
RUN gradle build -x test --no-daemon

# ステップ 2: 実行ステージ
FROM eclipse-temurin:21-jdk

# アプリケーションを実行するユーザーを作成
RUN useradd -m -u 1001 spring

# 作業ディレクトリを設定
WORKDIR /app

# ビルドステージからビルド済みのアプリケーションをコピー
COPY --from=build /app/build/libs/*.jar app.jar

# アプリケーションファイルの所有権を変更
RUN chown -R spring:spring /app

# 作成したユーザーとしてアプリケーションを実行
USER spring

# アプリケーションを実行するコマンドを指定
ENTRYPOINT ["java", "-jar", "app.jar"]

# アプリケーションポートを公開
EXPOSE 8080

```
上記のDockerfileはChatGPT 4oに`kotlin、spring boot、gradleのアプリをdockerコンテナ化するDockerfileのベストプラクティスを教えてください`と指示して作成してもらいました。

Dockerイメージのバージョンが古かったり、存在しないイメージを使ったりしていましたが、それ以外の修正はしていません。

`RUN gradle build`を2回している理由は、1回目で依存関係をビルドして2回目でソースコードのビルドをするためです。依存関係とソースコードのビルドを分けることで、ソースコードのみ変更した場合、**依存関係のビルドをDockerキャッシュで省略**できます。
# DockerでSpring BootとMySQLの連携
`docker-spring-mysql-app/compose.yaml`に先ほどのDockerfileを使ってDockerコンテナを立ち上げるよう設定を追記します。
```yaml:compose.yaml
services:
  backend:
    build:
      context: backend
    ports:
      - '8080:8080'
    depends_on:
      db:
        condition: service_healthy
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - APP_DATABASE_USER=${APP_DATABASE_USER}
      - APP_DATABASE_PASSWORD=${APP_DATABASE_PASSWORD}
    restart: always
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  db:
    image: mysql:8.1.0
    restart: always
    healthcheck:
      test:
        [
          'CMD-SHELL',
          'mysqladmin ping -h 127.0.0.1 --password="$$(cat /run/secrets/db-password)" --silent',
        ]
      interval: 3s
      retries: 5
      start_period: 30s
    secrets:
      - db-password
    volumes:
      - db-data:/var/lib/mysql
      - ./db/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
    # 本当は3306にマッピングしたいが、ホストがすでに使っているため3307
    ports:
      - '3307:3306'
    environment:
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password
      - TZ=Asia/Tokyo
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  db-data:

secrets:
  db-password:
    file: db/password.txt

```

これでMySQL、Spring BootのDockerコンテナを立てて連携する準備完了です。

`docker compose up --build -d`でコンテナを立ち上げ、ブラウザから`localhost:8080`にアクセスすると`テスト`が画面に表示されます。

ただ、`docker compose up`でアプリを起動するとIntelliJでデバックができません。デバックしたいときは以下のIntelliJ公式ドキュメントの設定をすればIntelliJからDockerコンテナを立ち上げられるようになります。

https://pleiades.io/help/idea/run-and-debug-a-spring-boot-application-using-docker-compose.html

以下の記事では、このアプリをVPS環境にデプロイして独自ドメイン、SSL設定をする手順を解説しています。

(後日公開予定)