はじめに
ポートフォリオとして個人開発したWebアプリを公開する過程でいくつかつまずいたポイントがあったため、備忘録的に残しておきたいと思います。
本シリーズでは、Spring BootアプリをDockerコンテナ化し、GitHub Action + GHCRでCIワークフローを構築し、AWS EC2上でデプロイして公開する までの流れを紹介します。
第1編の本記事では、アプリのDockerコンテナ化〜CIワークフローの構築までの流れを記載しております。
🛣️ 公開までのプロセス
前述のとおり、本記事では以下のプロセスのうち 「2. アプリをDockerコンテナ化」から「4. GHCRにDockerイメージを保存」まで を説明していきます。
-
アプリを開発(割愛)
--- ▼ ここから ▼--- - 📦 アプリをDockerコンテナ化
- 🗂️ ローカルでコンテナを起動
-
💾 GHCRにDockerイメージを保存
--- ▲ ここまで ▲ --- - VPCを作成
- EC2インスタンスを作成
- EC2上でイメージをGHCRからpullする
- コンテナを起動する
- ELBを作成
独自ドメインを取得(割愛)- Route 53を設定
🟣 インフラ構成図
今回は下図の構成でアプリを公開します。本記事では右側の 「ローカル開発環境を本番環境に適応させる準備」 を行います。
📦 アプリをDockerコンテナ化する
1. Dockerfileを作成
アプリをDockerコンテナ化するためには、Dockerイメージを作成する必要があります。
❓ Dockerイメージって何?
Dockerイメージとは、アプリの実行に必要な情報をまとめたテンプレートファイルです。Dockerイメージを基にコンテナが作成されます。
Dockerイメージを作成するために、設計図となる Dockerfile をルートディレクトリに作成します。
- イメージを軽量化するためにマルチステージビルドを採用
- Build StageでGradleを使用してアプリケーションをビルドしてJarファイルを生成
- Run StageでそのJarファイルをコピーしてアプリケーションを実行
- アプリケーション実行用に8080ポートを公開
# ビルド用と実行用を分けてイメージを軽量化する
# ------------
# Build Stage
# ------------
FROM gradle:8.10-jdk21-alpine AS builder
WORKDIR /app
# キャッシュの依存関係を使用してビルド時間を短縮(変更がない場合)
COPY build.gradle settings.gradle gradle* ./
RUN gradle dependencies --no-daemon || true
COPY src ./src
RUN gradle clean bootJar --no-daemon
# ----------
# Run Stage
# ----------
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN apk add --no-cache tzdata
ENV TZ=Asia/Tokyo
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
❓ マルチステージビルドの何が良いの?
マルチステージビルドにすることで、ビルド環境と実行環境を分離することができ、Run StageでJarファイルのみをコピーすることで、ビルドツールやソースコードが最終イメージに含まれず、セキュリティの向上や最終イメージサイズの軽量化が期待できます。
2. .dockerignoreを作成
設計図( Dockerfile )ができたら、今度は但し書き( .dockerignore )をルートディレクトリに作成します。
.dockerignore はいわゆる 「※ただし、⚪︎⚪︎は除く」 というやつです。
.dockerignore で指定することで、不要なファイルやディレクトリがDockerイメージに含まれるのを防ぐことができます。
- バージョン履歴やビルドキャッシュ、IDE関連、環境変数ファイルなどを除外
### バージョン管理 ###
.git
.gitignore
### IntelliJ IDEA ###
.idea
.DS_Store
### ビルド成果物 ###
build
out
### キャッシュ ###
.gradle/
### 環境変数ファイル ###
*.env
これで、アプリをコンテナ化する準備が整いました。
🗂️ ローカルでコンテナを起動する
それでは、実際にローカルでコンテナを起動して実行確認を行いましょう。
構成図のとおり、今回はアプリコンテナとDBコンテナを使用するため、複数のコンテナをまとめて動かすための指示書である docker-compose.yml をルートディレクトリに作成します。
1. docker-compose.ymlを作成
-
build: .でDockerイメージをビルド -
depends_on: - dbで依存関係を定義し、app → dbの順に起動 - パスワードなどは環境変数に置き換えて記載
services:
# アプリコンテナの設定
app:
build: .
depends_on:
- db
environment:
SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
# ローカルの8080ポートが空いていなかったので8081を割り当て
ports:
- "8081:8080"
# DBコンテナの設定
db:
image: mysql:8.0.42
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: awesome_collect
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Tokyo
# ローカルの3306ポートが空いていなかったので3307を割り当て
ports:
- "3307:3306"
volumes:
- db_data:/var/lib/mysql
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
volumes:
db_data:
パスワードなどを application.properties に直書きしている場合は、同様に環境変数に置き換えておきましょう。
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
2. .envを作成
指示書( docker-compose.yml )ができたら、今度は別紙( .env )をルートディレクトリに作成します。
docker-compose.yml や application.properties は .env を参照して環境変数の値を取得します。
SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/awesome_collect
SPRING_DATASOURCE_USERNAME=Your_DB_Username
SPRING_DATASOURCE_PASSWORD=Your_DB_Password
MYSQL_ROOT_PASSWORD=Your_DB_Root_Password
MYSQL_USER=Your_DB_Username
MYSQL_PASSWORD=Your_DB_Password
.env は .dockerignore や .gitignore で除外ファイルとして指定し、外部に公開しないようにしてください
3. コンテナを起動
コンテナを動かす準備が整ったので、実際に起動してみましょう。
## -dオプションをつけてバックグラウンドで実行
$ docker compose up -d
実行結果

アプリコンテナ・DBコンテナともに 「Started」 となり、コンテナが起動したことがわかります。
念のため、起動中のコンテナを確認してみます。
$ docker container ls
## または
$ docker ps
実行結果

STATUSが「Up」になっておりコンテナが起動していること、PORTSでポートが正しく割り当てられていることが確認できました。
4. 実行確認
コンテナを起動できたらブラウザからアクセスして実行確認を行います。今回は8081ポートに割り当てているため、http://localhost:8081 にアクセスしてアプリを開くことができれば成功です!
💾 GHCRにDockerイメージを保存する
次に、Dockerイメージを安全に管理・共有することができるように、GHCR に保存します。GHCRに保存されたDockerイメージを本番環境上でpullすることで、円滑に同じイメージを使用することができます。
❓ GHCRって何?
GHCR (GitHub Container Registry) とは、GitHubが提供するコンテナレジストリのことです。
GitHubアカウント内にコンテナイメージを格納し、イメージとリポジトリを関連づけることができます。公式Docはこちら↗︎
今回はGitHub Actionsのワークフローを用いてDockerイメージをビルドしてGHCRにpushする処理(CI)を自動化し、GHCRからDockerイメージをpullしてコンテナを起動する処理(CD)は手動で行うことにしました。
CDワークフローの構築を見送った理由
- 公開後もフィードバックを基にバグ修正・機能改善を円滑に行うため、SSH接続が可能なパブリックサブネットにEC2を設置
- ただし、セキュリティを考慮しSSH接続は自分のIPのみに制限
- CDワークフローを構築するにはGitHub管理下のIPからのSSH接続を許可する必要がある
- プライベートサブネットでの運用も検討したが、学習段階での運用・保守性を優先し、SSM導入は見送り
- 自己ホストランナーを使用してのCD構成も検討したが、ポートフォリオ用のパブリックリポジトリのためセキュリティリスクを考慮して除外
❓ ワークフローって何?
ワークフローとは、「こういうことがあったら(トリガー)、これを実行してね(ジョブ)」ということを定義しておくことのできる、業務マニュアルのようなものです。
トリガーとジョブを設定しておくことで、特定の処理を自動化することができます。公式Docはこちら↗︎
1. GitHub Actions のワークフローを作成
GitHubリポジトリで [Actions] → [New workflow] の順にクリックすると、テンプレートを選択することができるため、今回はこのテンプレートを基に作成してみようと思います。
「Docker image」 を選択して開くと、次のようなYAMLファイルが表示されます。
name: Docker Image CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build the Docker image
run: docker build . --file Dockerfile --tag my-image-name:$(date +%s)
これをベースに、今回のアプリ用に修正を加えます。
どういうときに(トリガー)、何を実行したいのか(ジョブ)を整理して、順番に記載していきます。
- トリガー: GitHubリポジトリのmainブランチにコミットがpush(またはPRがマージ)されたとき
- ジョブ: ① GHCRにログイン → ② Dockerイメージをビルド → ③ GHCRにDockerイメージをpush
name: Docker Image CI
## トリガー: mainブランチにpushしたとき
## PRマージ前の実行は不要のため、テンプレートからpull_requestを削除
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
## GHCRにpushする権限を付与
permissions:
contents: read
packages: write
steps:
## リポジトリのコードをコピー
- name: Checkout code
uses: actions/checkout@v4
## ジョブ①: GHCRにログイン
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
## ジョブ②: Dockerイメージをビルド
- name: Build the Docker image
run: docker build -t ghcr.io/${{ github.repository_owner }}/awesome_collect:latest .
## ジョブ③: GHCRにDockerイメージをpush
- name: Push image to GHCR
run: docker push ghcr.io/${{ github.repository_owner }}/awesome_collect:latest
右上の [Commit changes...] をクリックすると、リポジトリに 「.github/workflows」 ディレクトリが追加され、 docker-image.yml が保存されます。
docker-image.yml がリポジトリに追加されたということは、「mainブランチにpushされた」ことになるので、トリガーを検知してワークフローが実行されているはずです。もう一度、 [Actions] タブを見てみましょう。
ワークフローが実行されると、トリガーであるアクションと実行ステータスが表示されます。
オレンジの丸🟠は 「実行中」 、緑に✔︎が入った丸🟢は 「成功」 、赤に✖︎が入った丸🔴は 「失敗」 を表しています。
ワークフローの実行失敗例
実は、上記の docker-image.yml にする前にワークフローの実行に失敗しています。
ワークフローの実行に失敗した場合、どの工程でどんな理由で失敗したのか、詳細情報を確認することができます。
1. 失敗したアクション(下図)をクリック
下図が表示され、 docker-image.yml の on: push 工程でエラーが起こったことがわかります。
2. 失敗した工程(上図[build])をクリック
下図のように、ワークフローのうち実行に失敗した工程が展開され、エラーの内容を確認することができます。
このケースでは、リポジトリ名を大文字を含む文字列にしてしまっていたことが原因でした。コンテナイメージのリポジトリ名はすべて小文字である必要があります。
## GitHubのリポジトリ名を"AwesomeCollect"と大文字を含んだ形式にしており、
## そのリポジトリ名を${{ github.repository }}で参照していた
- name: Build the Docker image
run: docker build -t ghcr.io/${{ github.repository }}:latest .
以下のように、GitHubリポジトリのオーナー名のみを参照し、リポジトリ名は参照せず明示的に記載することで、ワークフローの実行を成功させることができました。
## GitHubリポジトリのオーナー名のみ${{ github.repository_owner }}で参照
## リポジトリ名は"awesome_collect"と明示的に記載
- name: Build the Docker image
run: docker build -t ghcr.io/${{ github.repository_owner }}/awesome_collect:latest .
2. パッケージを確認
GitHub Actionsのワークフローで${{ secrets.GITHUB_TOKEN }}を利用してコンテナイメージをpushすると、ワークフローが含まれるリポジトリがそのイメージに自動的にリンクされます。
リポジトリとリンクされたコンテナイメージは、リポジトリページの [Packages] から確認することができます。
作成したコンテナイメージ(パッケージ)をクリックすると、コマンドラインからpullするためのコマンドや、イメージのバージョン履歴などを確認することができます。
リポジトリとリンクされたパッケージには、そのリポジトリの可視性が継承されます。
今回は、パブリックリポジトリのため、pushされたコンテナイメージも同様にパブリックパッケージとなります。
(※ 必要に応じてあとからパッケージ単体で可視性を変更することも可能です)
おわりに
これで、本番環境で使用するためのコンテナの準備がすべて整いました。
次回の第2編では、いよいよアプリを実際に公開するためのAWS環境構築に挑戦します。
インフラ構成の選定理由や、EC2の設定手順などを紹介していきます。
最後までお読みいただきありがとうございました。






