Spring BootとPostgreSQLのDocker化

![docker-logo-blue.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4041306/fa304a34-c992-4fa6-87f5-b5ad27feb50d.png)

> 本記事のDockerロゴは、公式ガイドラインに沿って使用しています。 
> [https://www.docker.com/ja-jp/company/newsroom/media-resources/](https://www.docker.com/ja-jp/company/newsroom/media-resources/)



## はじめに
ポートフォリオを作成していく中で、「自分の環境では動くが、採用担当者の環境では動かない」という可能性を考えました。Java のバージョン違いやデータベースの設定差異、OSの違いなどが原因で、せっかく作ったアプリケーションが評価してもらえないリスクがあります。そこで Docker を使用して環境を統一し、どの環境でも確実に動作するようにしました。
Dockerに触れたことのない方でも、「プロジェクトのセットアップ」セクションの手順に従うことで、**簡単**にアプリケーションを起動できます。ハンズオンでの体験ができますので、ぜひお試しください。



## ディレクトリ構成

```bash
expensecalendar/
├── .env.example          # 環境変数のテンプレート（ダミーパスワードを記述）
├── .env                  # 実際の環境変数（.gitignoreに含める）
├── expensecalendar-backend/
│   ├── Dockerfile        # イメージ
│   ├── build.gradle      # backendの設定
│   ├── settings.gradle
│   └── src/
│       └── main/
│           └── resources/
│               └── application-dev.properties  # 開発環境用DB設定
├── build.gradle          # rootの設定
├── compose.yaml          # Docker Compose設定
└── settings.gradle
```



## `.env`

```bash
# Spring Bootのプロファイル
SPRING_PROFILES_ACTIVE=dev
 
# PostgreSQLの設定（Dockerコンテナ用）
POSTGRES_DB=expensecalendar_db
POSTGRES_USER=springuser
POSTGRES_PASSWORD=mypassword

# Spring Bootアプリケーションのデータソース設定
SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/expensecalendar_db
SPRING_DATASOURCE_USERNAME=springuser
SPRING_DATASOURCE_PASSWORD=mypassword
```

こちらは実際に使用する環境変数の設定です。
#### 重要ポイント
- **PostgreSQL設定**: Dockerコンテナの初期化に使用
- **Spring Boot設定**: アプリケーションからのDB接続に使用
- **ホスト名**: `localhost`ではなく`db`（Docker Composeのサービス名）を指定
- **パスワード**: 実際に使用するものを記述し、必ず`.gitignore`に含めてください


#### ✅`.env`ファイルを使用するメリット

- **設定の一元管理**: 環境変数を1つのファイルで管理
- **セキュリティ**: 機密情報をコードから分離
- **環境切り替え**: 開発・本番環境の設定を簡単に変更
- **チーム開発**: `.env.example`でテンプレート共有

#### Docker Secretsを使わない理由
**本番環境**ではDocker Secretsの使用が推奨されますが、今回の目的は「ローカルでの環境の固定」と「機密情報をコードから分離」することにあるため、`.env`ファイルを採用しています。

> ※Docker Secretsについて詳しく知りたい方は、「参考文献」セクションの書籍や公式ドキュメントをご参照ください。



## `.env.example`

```bash
# Spring Bootのプロファイル
SPRING_PROFILES_ACTIVE=dev

# PostgreSQLの設定（Dockerコンテナ用）
POSTGRES_DB=expensecalendar_db
POSTGRES_USER=springuser
POSTGRES_PASSWORD=yourpassword

# Spring Bootアプリケーションのデータソース設定
SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/expensecalendar_db
SPRING_DATASOURCE_USERNAME=springuser
SPRING_DATASOURCE_PASSWORD=yourpassword
```
GitHubに公開する**ダミーパスワード**が書かれた環境変数のテンプレートです。
採用担当者の方はコピーして`.env`を作成し、実際のパスワードに変更します。



## `application-dev.properties`

```properties
#== ローカル開発環境の設定 ==

#Postgresのドライバの設定
spring.datasource.driver-class-name=org.postgresql.Driver
#PostgresのURLの設定
spring.datasource.url=${SPRING_DATASOURCE_URL}
#Postgresのユーザ名の設定
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
#Postgresのパスワードの設定
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}

#SQLスクリプトの初期化の設定
spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.data-locations=classpath:data.sql

#Log表示設定
logging.level.com.ozeken.expensecalendar=DEBUG

# MyBatisのマッパーXMLの場所を指定
mybatis.mapper-locations=classpath:mapper/*.xml
# MyBatisで使うエンティティのパッケージ指定
mybatis.type-aliases-package=com.ozeken.expensecalendar.entity

# Thymeleafのキャッシュを無効にする
spring.thymeleaf.cache=false
```

開発環境用のDB設定です。

#### 重要ポイント
- **環境変数の活用**: `${SPRING_DATASOURCE_URL}`などのプレースホルダーに`.env`で定義した値が挿入される 
- **プロファイル**: `application-dev.properties`は`SPRING_PROFILES_ACTIVE=dev`で有効になる



## `gradleファイル`
:::note info
`backend`にプロジェクトをまとめてDocker化する場合、gradleファイルの設定に苦労したため、gradleファイルの記述も解説します。
:::


### rootの`build.gradle`

```gradle
// ==========================================
// Composite Build用 ルートプロジェクト設定
// expensecalendar-backendプロジェクトのタスクをルートから実行可能にする
// ==========================================


// ビルド成果物の削除
tasks.register("clean") {
    group = "build"
    description = "Cleans backend build outputs"
    dependsOn gradle.includedBuild("expensecalendar-backend").task(":clean")
}

// Javaソースとリソースのコンパイル
tasks.register("compile") {
    group = "build" 
    description = "Compiles backend application"
    dependsOn gradle.includedBuild("expensecalendar-backend").task(":classes")
}

// 単体テストの実行
tasks.register("test") {
    group = "verification"
    description = "Runs backend tests"
    dependsOn gradle.includedBuild("expensecalendar-backend").task(":test")
}

// コード品質チェック（静的解析等）
tasks.register("check") {
    group = "verification"
    description = "Runs all verification tasks"
    dependsOn gradle.includedBuild("expensecalendar-backend").task(":check")
}

// テストなしでの実行可能JARファイル作成（ローカル）
tasks.register("build") {
    group = "build"
    description = "Builds backend application without tests"
    dependsOn gradle.includedBuild("expensecalendar-backend").task(":bootJar")
}

// Heroku等のデプロイプラットフォーム用
tasks.register("stage") {
    group = "build"
    description = "Prepares application for deployment"
    dependsOn("build")
}
```
`dependsOn gradle.includedBuild("expensecalendar-backend").task()` :
rootからbackendをビルドするための設定です。


### rootの`settings.gradle`

```gradle
rootProject.name = 'expensecalendar'
includeBuild 'expensecalendar-backend'
```
`includeBuild 'expensecalendar-backend'` :
`expensecalendar-backend`を`expensecalendar`のサブプロジェクトと認識させるための設定です。

**この2つのgradleファイルでrootからbackendのプロジェクトをビルドすることができます。**

---

### backendの`gradleファイル`
通常のSpring Bootプロジェクトと同じ書き方のため、説明は省略します。



## `Dockerfile`

```Dockerfile
# Build stage
FROM eclipse-temurin:17-jdk AS build

WORKDIR /app

# 依存関係をキャッシュするためにビルドに必要なファイルを先にコピー
COPY expensecalendar-backend/build.gradle expensecalendar-backend/settings.gradle expensecalendar-backend/gradlew expensecalendar-backend/gradlew.bat ./
COPY expensecalendar-backend/gradle ./gradle

# gradlewに実行権限を付与（Linux環境なら必須）
RUN chmod +x gradlew

# ソースコードをコピー
COPY expensecalendar-backend/src ./src

# ビルド実行（デーモンを使わずクリーンビルド）
RUN ./gradlew clean build --no-daemon

# Runtime stage
FROM eclipse-temurin:17-jre

WORKDIR /app

# build stageからビルド済みjarだけをコピー
COPY --from=build /app/build/libs/*.jar app.jar

# アプリ起動
CMD ["java", "-jar", "app.jar"]
```
---


### 1. Build Stage（ビルドステージ）
```dockerfile
# Build stage
FROM eclipse-temurin:17-jdk AS build
```
- `FROM eclipse-temurin:17-jdk` : Java 17のJDK（開発用）をベースイメージとして使用
- `AS build`: このステージに「build」という名前を付ける（後で参照するため）

```Dockerfile
WORKDIR /app
```
- `WORKDIR` : コンテナ内の作業ディレクトリを`/app`に設定
- 以降のコマンドは全て`/app`ディレクトリで実行される
---


### 2. 依存関係のキャッシュ戦略
```Dockerfile
# 依存関係をキャッシュするためにビルドに必要なファイルを先にコピー
COPY expensecalendar-backend/build.gradle expensecalendar-backend/settings.gradle expensecalendar-backend/gradlew expensecalendar-backend/gradlew.bat ./
COPY expensecalendar-backend/gradle ./gradle
```
#### なぜ先にGradleファイルをコピーするのか

- Dockerは**レイヤーキャッシュ**という仕組みがあり、ファイルが変更されていなければ前回のビルド結果を再利用する
- `build.gradle`が変わらなければ、依存関係のダウンロードをスキップできる

#### コピーの順序が重要
1. 設定ファイル（変更頻度：低）→ キャッシュしやすい
1. ソースコード（変更頻度：高）→ 最後にコピー

**そのため設定ファイルを先にコピーしておきます。**

---


### 3. 実行権限の付与
```Dockerfile
# gradlewに実行権限を付与（Linux環境なら必須）
RUN chmod +x gradlew
```
`chmod +x` : Linuxコンテナでは明示的に権限設定が必要なため、**gradlew**ファイルに実行権限を与えます。

---


### 4. ソースコードのコピーとビルド

#### コピー
```Dockerfile
# ソースコードをコピー
COPY expensecalendar-backend/src ./src
```
- 変更が多いため、最後にソースコードをコピー

#### ビルド
```Dockerfile
# ビルド実行（デーモンを使わずクリーンビルド）
RUN ./gradlew clean build --no-daemon
```
- `clean build` : 古いビルド結果を削除してから新しくビルド
- `--no-daemon` : Gradleデーモンを使わない（コンテナ内では不要で、メモリ節約）

---


### 5. Runtime Stage（実行段階）
```Dockerfile
# Runtime stage
FROM eclipse-temurin:17-jre
```
- 実行段階ではJRE（実行用）をベースイメージとして設定
（JDKと比べて軽量なため）

```Dockerfile
WORKDIR /app
# build stageからビルド済みjarだけをコピー
COPY --from=build /app/build/libs/*.jar app.jar
```
- `--from=build` : 前のbuildステージからファイルを参照
- **重要** : ソースコードやGradleキャッシュは含まれない、JARファイルのみ

```Dockerfile
# アプリ起動
CMD ["java", "-jar", "app.jar"]
```
- コンテナ起動時に実行されるコマンド

---

### 6. 最終的な効果
この Dockerfile により以下が実現されます：
- **軽量なイメージ** : JRE + JARファイルのみ
- **高速ビルド** : キャッシュ戦略により依存関係の再ダウンロードを削減
- **安全性** : ソースコードが本番イメージに含まれない



## `compose.yaml`
```yaml
services:
  expensecalendar-backend:
    build:
      context: .
      dockerfile: expensecalendar-backend/Dockerfile
    env_file:
      - .env
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE}
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    ports:
      - "5432:5432"
```

#### サービス構成

- `expensecalendar-backend:` Spring Bootアプリケーションの設定
- `db:` PostgreSQLデータベースの設定

#### 重要ポイント

- `env_file:`: `.env`ファイルの環境変数を自動読み込み
- `depends_on:`: データベースが起動してからアプリケーションを開始
- `ports:`: ホストからコンテナへのポート転送（8080→8080、5432→5432）
- `build:`: `Dockerfile`からイメージをビルドしてSpring Bootアプリを実行



## プロジェクトのセットアップ

実際に動作を確認したい方は、以下をインストールし、リポジトリをクローンしてお試しください：



###  必要なツール

- **Docker Desktop (必須)**: コンテナ実行環境
- **pgAdmin (任意)**: データベースをGUIで確認


### Docker Desktop
[https://www.docker.com/](https://www.docker.com/)

### pgAdmin（任意）
[https://www.postgresql.jp/download](https://www.postgresql.jp/download)

データベースの中身をGUIで確認できるため、動作確認に便利です。


### リポジトリのクローンと`.env`の作成
```bash
git clone https://github.com/ktr0203ozeken/expensecalendar.git
cd expensecalendar
cp .env.example .env
nano .env 
```



## コンテナのビルド・起動

### 操作手順
```bash
docker compose up
```
コンテナをビルド＆起動します。初回は時間がかかる場合があります。

---

### ビルド確認ログ
```bash
[+] Running 3/3
 ✔ Network expensecalendar_default                      Created     0.0s
 ✔ Container expensecalendar-db-1                       Created     0.1s
 ✔ Container expensecalendar-expensecalendar-backend-1  Created     0.1s
```
コンテナの作成・起動が確認できます。

---

### DBログインログ
```bash
expensecalendar-backend-1  | ～～ : The following 1 profile is active: "dev"

expensecalendar-backend-1  | ～～ : HikariPool-1 - Starting...
expensecalendar-backend-1  | ～～ : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@13bdf540
expensecalendar-backend-1  | ～～ : HikariPool-1 - Start completed.
```
`dev`プロファイルが使われ`PostgreSQL`にログインできていることが確認できます。

---

### アプリケーション起動のログ
```bash
expensecalendar-backend-1  | ～～ : Tomcat started on port 8080 (http) with context path '/'
expensecalendar-backend-1  | ～～ : Started ExpensecalendarApplication in 2.621 seconds
```
アプリケーションが正常に起動し、`8080ポート`で利用可能になります。



## コンテナの停止・削除

### 1. コンテナの停止
実行中のコンテナを停止するには、ターミナルで `Ctrl + C` を押します。

### 2. コンテナの削除
```bash
docker compose down
```

---

### 停止ログ
```bash
[+] Stopping 2/2
 ✔ Container expensecalendar-expensecalendar-backend-1  Stopped     0.5s
 ✔ Container expensecalendar-db-1                       Stopped     0.5s
```
コンテナが停止されていることが確認できます。

### 削除ログ
```bash
[+] Running 3/3
✔ Container expensecalendar-expensecalendar-backend-1  Removed     0.0s
✔ Container expensecalendar-db-1                       Removed     0.0s
✔ Network expensecalendar_default                      Removed     0.6s
```
コンテナとネットワークが正常に削除されていることが確認できます。



## まとめ
このようにDockerを使用しイメージの固定化をすることで、「**自分の環境では動くが、他人の環境では動かない**」という横断的関心事を解決することができます。
皆さんの参考になったら幸いです。



## 参考文献
本記事の執筆にあたり、以下の資料を参考にしました。

[Docker/Kubernetes実践コンテナ開発入門 改訂新版](https://www.amazon.co.jp/gp/product/B0CVLFLD7L?ie=UTF8&psc=1)