Dockerイメージを軽くする「マルチステージビルド」について

前回の記事では、複数のコンテナを操る docker-compose.yml の書き方を紹介した。今回は、そのコンテナの中身を定義する Dockerfile について書く。

今回作成したバックエンド(Java/Spring Boot)とフロントエンド(React/Node.js)のDockerfileには、「マルチステージビルド」と使っている。

これを使うと、「イメージサイズが小さくなり、セキュリティも向上する」

バックエンド編:Java (Spring Boot)

Javaアプリを動かすには、ビルドツールの「Maven」や「Gradle」が必要だが、実行時にこれらは不要となる。そこで、ビルドする段階と、実行する段階を分ける。

# --- Stage 1: ビルド環境 ---
FROM maven:3.9-eclipse-temurin-25 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
# テストはCIでやる前提でスキップし、ビルド時間を短縮
RUN mvn clean package -DskipTests

# --- Stage 2: 実行環境 ---
FROM eclipse-temurin:25-jre
WORKDIR /app
# ビルド環境(Stage 1)で作った成果物(jar)だけをコピー
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

  • AS build と COPY –from=build: 最初の FROM で「build」という名前をつけ、そこで mvn package して jarファイルを作る。2つ目の FROM が本番用イメージ。ここには Maven もソースコードも含まれない。あるのは 「JRE(Java実行環境)」と「jarファイル」だけ。 これにより、数百MB単位で容量を節約できる。
  • eclipse-temurin:25-jre: JDK(開発キット)ではなく、JRE(実行環境)を選んでいる。コンパイラなどが含まれないため、攻撃者に悪用されるリスクが減る。

フロントエンド編:Node.js (React/Vueなど)

フロントエンド開発では npm run dev サーバーを使うが、本番環境でNode.jsサーバーをそのまま動かすのはパフォーマンス的に最適ではない。 ビルドした静的ファイル(HTML/CSS/JS)を、高速なWebサーバーである Nginx に配信させる。

# --- Stage 1: ビルド環境 ---
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
# npm install より高速で、lockファイルに忠実な ci を使用
RUN npm ci
COPY . .
RUN npm run build

# --- Stage 2: 配信環境 (Nginx) ---
FROM nginx:alpine
# ビルド成果物(dist)をNginxの公開ディレクトリに配置
COPY --from=build /app/dist /usr/share/nginx/html
# SPA用の設定ファイルをコピー
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

  • Node.js はビルド時のみ使用: Reactなどのコードはブラウザで動くJavaScriptに変換(トランスパイル)する必要がある。これには Node.js が必要だが、変換が終われば Node.js 自体は不要となる。 最終的なイメージは nginx:alpine ベースになるため、サイズはわずか数十MBに収まる。
  • npm ci の採用: npm install ではなく npm ci を使っている。これは package-lock.json を厳密に守ってインストールを行うコマンドで、CI/CDやDockerビルドでの再現性を保証する。
  • カスタムNginx設定: ReactのようなSPA(シングルページアプリケーション)では、どのURLにアクセスしても index.html を返す設定が必要になるため、デフォルト設定を上書きしている。

まとめ

今回作成した2つのDockerfileに共通するのは、「作る道具(Maven/Node)」と「動かす場所(JRE/Nginx)」を明確に分けている点となる。

Docker Composeで、Webアプリ開発環境を作る

今回は、典型的な「フロントエンド(React等) + バックエンド(Spring/Node等) + データベース(MariaDB)」の構成を例に docker-compose.yml を書いた。

今回の構成

3つのコンテナ(Service)が専用のネットワーク内で連携し、データはボリュームによって永続化される構成。

完成した docker-compose.yml

以下が今回解説する設定ファイル。例として小説検索システム(Novel Search System)のようなアプリケーションを想定している。

services:
  backend:
    build: ./backend
    container_name: novel-backend
    ports:
      - "8080:8080"
    environment:
      - DB_URL=jdbc:mariadb://novel-db:3306/${DB_NAME}
      # ... (省略)
    depends_on:
      novel-db:
        condition: service_healthy # ここが重要!
    networks:
      - novel-network

  frontend:
    build: ./frontend
    container_name: novel-frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - novel-network

  novel-db:
    image: mariadb:12.1
    container_name: novel-db
    environment:
      - MARIADB_DATABASE=${DB_NAME}
      # ... (省略)
    healthcheck:
      test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$MARIADB_ROOT_PASSWORD || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 10
    volumes:
      - db_data:/var/lib/mysql
      - ./backend/sql:/docker-entrypoint-initdb.d
    networks:
      - novel-network

networks:
  novel-network:
    driver: bridge

volumes:
  db_data:

1.データベースの「起動待ち」を完璧にする healthcheck

Docker Composeでよくあるトラブルが、「DBが完全に立ち上がる前にバックエンドが接続に行き、エラーで落ちる」という現象。

単なる depends_on: – novel-db だけでは、「コンテナが作成された」ことしか確認できず、「DB接続の準備ができた」ことまでは保証されない。

そこでHealthcheck(ヘルスチェック) を行う。

novel-db:
    # ...
    healthcheck:
      test: ["CMD-SHELL", "mariadb-admin ping ..."]
      interval: 10s
      retries: 10

ここでは mariadb-admin ping コマンドを定期的に実行し、DBが応答するかを監視している。

backend側では以下のように記述する。

backend:
    depends_on:
      novel-db:
        condition: service_healthy

これにより、「DBのヘルスチェックがOKになるまで、バックエンドを起動させない」という制御が可能になる。

2.データの永続化と初期データの投入

データベースをコンテナ化する際、コンテナを削除するとデータも消えてしまう。これを防ぐのが volumes 。

volumes:
      - db_data:/var/lib/mysql
      - ./backend/sql:/docker-entrypoint-initdb.d

  • db_data:/var/lib/mysql: ホスト側(Docker管理領域)にデータを保存する。コンテナを作り直しても、小説データやユーザーデータは消えない。
  • ./backend/sql:/docker-entrypoint-initdb.d: MariaDBやMySQLの公式イメージは、/docker-entrypoint-initdb.d に置かれた .sql ファイルを、初回起動時に自動実行してくれる。 テーブル作成や初期データの投入を自動化できるため、開発メンバー全員が docker compose up 一発で同じDB状態から開発をスタートできる。

EclipseからVS Codeに開発環境を移した

フロントエンドの開発をしたかったので、VS Codeを触ってみた。

殆ど問題なく作業を進めることが出来たが、一部戸惑った点を書き残しておく。

起動構成はJSON形式で書く

{
    "configurations": [
        {
            "type": "java",
            "name": "Spring Boot App",
            "request": "launch",
            "mainClass": "com.example.novel.NovelApplication",
            "projectName": "",
            "env": {
                "DEV_DB_URL": "jdbc:mariadb://url",
                "DEV_DB_USERNAME": "id",
                "DEV_DB_PASSWORD": "pass"
            }
        },
        {
            "type": "node",
            "name": "React App",
            "request": "launch",
            "runtimeExecutable": "npm",
            "runtimeArgs": [
                "run-script",
                "dev"
            ],
            "cwd": "${workspaceFolder}/frontend",
            "console": "integratedTerminal"
        }
    ]
}

1. ひとつ目の設定:Java (Spring Boot) アプリケーション用

Javaのプログラム(Spring Bootなど)を起動するための設定。

  • "type": "java"
    • Java拡張機能を使ってデバッグ
  • "name": "Spring Boot App"
    • VS Codeの「実行とデバッグ」画面のプルダウンメニューに表示される名前
  • "request": "launch"
    • 新しくプログラムを起動するという意味(対義語は "attach" で、既に動いているプロセスに接続する場合に使う)
  • "mainClass": "com.example.novel.NovelApplication"
    • プログラムのエントリーポイント(public static void main メソッドがあるクラス)を指定
  • "projectName": ""
    • 通常、VS Codeが認識しているJavaプロジェクト名(フォルダ名など)が入る
    • 空欄 "" のままだと、複数のプロジェクトがある場合に起動できない可能性がある。1つだけなら問題なし
  • "env"
    • プログラム実行時にOSの環境変数として渡す値を設定している
    • アプリケーション側(application.properties など)で、DB接続情報をハードコードせずに、ここから読み込むようにしている
      • DEV_DB_URL: 接続先データベースのURL
      • DEV_DB_USERNAME: データベースのユーザーID
      • DEV_DB_PASSWORD: データベースのパスワード

2. ふたつ目の設定:Node.js (NPM) アプリケーション用

フロントエンド開発(React, Vueなど)の開発サーバーを起動するための設定。

  • "type": "node"
    • Node.js用のデバッグ
  • "name": "React App"
    • VS Codeの「実行とデバッグ」画面のプルダウンメニューに表示される名前
  • "request": "launch"
    • 新しくプログラムを起動するという意味
  • "runtimeExecutable": "npm"
    • 直接 node コマンドを実行するのではなく、npm コマンドを実行する
  • "runtimeArgs": [ "run-script", "dev" ]
    • npm コマンドに渡す引数
    • つまり、ターミナルで npm run-script dev (または単に npm run dev)と打つのと同じコマンドが実行される
  • "cwd": "${workspaceFolder}/frontend"
    • Current Working Directory(作業ディレクトリ)の指定
    • ${workspaceFolder} はVS Codeで開いているルートフォルダを指す
  • "console": "integratedTerminal"
    • ログの出力先を「デバッグコンソール」ではなく、VS Codeの**「統合ターミナル」**にする

VS Codeの設定はJSON形式で書く

{
    "java.configuration.updateBuildConfiguration": "automatic",
    "java.compile.nullAnalysis.mode": "automatic",
    "maven.terminal.customEnv": [
        {
            "environmentVariable": "DEV_DB_URL",
            "value": "jdbc:mariadb://url"
        },
        {
            "environmentVariable": "DEV_DB_USERNAME",
            "value": "id"
        },
        {
            "environmentVariable": "DEV_DB_PASSWORD",
            "value": "pass"
        }
    ]
}

1. Javaのビルド・解析設定

VS CodeがJavaプロジェクトをどう扱うかという、エディタの挙動設定

  • "java.configuration.updateBuildConfiguration": "automatic"
    • 意味: pom.xml(や build.gradle)が書き換えられた際、自動的にプロジェクト構成を更新する
    • Eclipseで言うと: プロジェクトを右クリックして行う 「Maven > プロジェクトの更新 (Alt+F5)」を、保存のたびに全自動でやってくれる機能
    • メリット: 依存ライブラリを追加した際、いちいち手動更新しなくてもすぐにimportできるようになる
  • "java.compile.nullAnalysis.mode": "automatic"
    • 意味: Javaのコードに対するNull(ヌル)分析機能のモード
    • 動作: プロジェクト内にNull注釈(@Nullable@NonNull など)が含まれている場合、自動的にNullポインタ参照の警告を出してくれる
    • メリット: 「ここでNullになる可能性があります」という警告がエディタ上で出るようになり、NullPointerExceptionを未然に防ぎやすくなる

2. Maven実行時の環境変数設定

  • "maven.terminal.customEnv": [ ... ]
    • 意味: VS CodeのMaven拡張機能を使ってコマンド(例: mvn spring-boot:runmvn test)を実行する際、一時的にセットする環境変数を定義

SonarQubeのPort番号変更方法

動機

テレワークでZscalerを使用している場合、SonarQubeがデフォルトで使用するポート番号9000がZscalerに占有されてしまっている。SonarQubeのポート番号を変更する必要があった。

環境

Windows 11
Java 17
Apache Maven 3.9.1
SonarQube 9.9 LTS

SonarQubeサーバー側

インストールフォルダのconf下にある、sonar.propertiesでPort番号を設定できる。

# TCP port for incoming HTTP connections. Default value is 9000.
#sonar.web.port=9000

以下の通り、空いているPort番号に変更する。

# TCP port for incoming HTTP connections. Default value is 9000.
sonar.web.port=9090

SonarQubeクライアント側(スキャナー側)

自分は以下の通りバッチ起動時の環境変数に設定した。

set MAVEN_OPTS=-Xmx2048m -Dsonar.host.url=http://localhost:9090
call mvn sonar:sonar

Eclipseのフォントを変更する

いつもEclipseでコーディングするときは、MS ゴシックを使用していた。しかし、最近になってもっと見やすいフォントに変更したいと思うようになった(歳のせいかもしれない)。その過程で、プログラミング向けフォントというものがあることを知った。

これらのプログラミング向けフォントは幾つかの種類が公開されているが、今回は日本語に対応している「UDEV Gothic」を試してみることにした。

フォントのインストール

ダウンロードしたファイルを解凍して、右クリックのメニューからインストールをクリックする。UDEV Gothic内にも色々なスタイルが存在しているが、標準で良ければ以下の選択で良い。

Eclipseのフォントの変更

Eclipseの設定から、一般>外観>色とフォントを選択し、「テキスト・エディター・ブロック選択フォント」と「テキスト・フォント」のフォントをUDEV Gothicに変更する。

適用して閉じると以下の通りエディターのフォントが変わる。
変更前(MSゴシック):

変更後(UDEV Gothic):

かなり見やすくなったと思う。

EclipseでGitの作成者、コミッターの初期設定を変更する

EclipseでGitを使用するとき、作成者、コミッターの欄に、OSのユーザー、コンピューター名が設定されてしまう。コミットする度に修正するのは面倒なので、最初からほしい名称が表示されるようにしたい。

Eclipseの設定のGit関連の設定項目の中から「構成」を選択する。エントリーの追加をクリックし、以下の項目を追加する。

user.name

user.email

Eclipseを一度再起動すると、先ほど設定した値が作成者、コミッターの欄に表示されるようになる。

Vue3でvue-i18nの初期化でエラーになる

以下のようなエラーが出ていた。

Uncaught (in promise) SyntaxError: Not available in legacy mode
    at createCompileError (message-compiler.esm-bundler.js?965a:54:1)
    at createI18nError (vue-i18n.esm-bundler.js?666d:100:1)
    at useI18n (vue-i18n.esm-bundler.js?666d:2228:1)
    at setup (NovelSearch.vue?0b40:12:1)
    at callWithErrorHandling (runtime-core.esm-bundler.js?d2dd:155:1)
    at setupStatefulComponent (runtime-core.esm-bundler.js?d2dd:7165:1)
    at setupComponent (runtime-core.esm-bundler.js?d2dd:7119:1)
    at mountComponent (runtime-core.esm-bundler.js?d2dd:5473:1)
    at processComponent (runtime-core.esm-bundler.js?d2dd:5448:1)
    at patch (runtime-core.esm-bundler.js?d2dd:5038:1)

Vue 3のsetupでuseI18nを構成する場合は、createI18nのlegacyオプションをfalseに設定する必要がある。

const i18n = createI18n({
  legacy: false, // you must set `false`, to use Composition API
  locale: 'ja',
  messages: {
    en: enNames,
    ja: jaNames
  }
});

const app = createApp(App)
app.use(i18n)
app.mount('#app')

参考:https://vue-i18n.intlify.dev/guide/advanced/composition.html

Eclipseで特定のWarningを非表示にしたい

現象

Eclipseでは属性名の間違い等を警告してくれる便利な機能があるが、新しい属性名に対応していないことがあり、正しい値なのにWarningが出てしまうことがある。

下記の例だと、「integrity」、「crossorigin」、「referrerpolicy」が未定義とWarningが出てしまっている。

Warningを非表示にする方法

  1. プロジェクトのプロパティから、検証のHTML構文を開く。
  2. 「プロジェクト固有の設定を可能にする」にチェックを入れる。
  3. 「指定された属性名を検証で無視」にチェックを入れる。
  4. 無視する属性名をカンマ区切りで入力する。

結果

下記の通りWarningは表示されなくなった。

CircleCIとCOVERALLSを連携する

やりたいこと

CircleCIでテスト実行後、code coverageをCOVERALLSに表示したい。

COVERALLSの設定

レポジトリを登録して、SettingsのREPO TOKENをコピーする。

CircleCIの設定

先ほど確認したREPO TOKENをProject SettingのEnvironment VariablesにCOVERALLS_REPO_TOKENとして登録する。

config.ymlの設定

MavenのGoalにjacoco:report coveralls:reportを指定する。

      # Generate a site.
      - run:
          name: Site
          command: mvn jacoco:report coveralls:report
      - store_artifacts:
          path: target/site
          destination: reports

pom.xmlの設定

repoTokenにCOVERALLS_REPO_TOKENを設定する。

            <plugin>
                <groupId>org.eluder.coveralls</groupId>
                <artifactId>coveralls-maven-plugin</artifactId>
                <version>4.3.0</version>
                <configuration>
                    <repoToken>${COVERALLS_REPO_TOKEN}</repoToken>
                </configuration>
            </plugin>

動作確認

対象のGitHubレポジトリにpushすると、ビルドが動いた後、下記の通りcode coverageが表示されることが確認できた。

CircleCIのビルド後テスト結果が表示されるようにしたい

Junitのテスト結果を表示

Maven Testの後にJunitのテスト結果ファイルを収集する設定を追加する。

      # Publish test results.
      - run:
          name: Collect test results
          command: |
            mkdir -p ~/junit/
            find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} ~/junit/ \;
          when: always
      - store_test_results:
          path: ~/junit

下記の通りテスト結果が表示されるようになる。

Siteを表示

Mavenで実行したCheckStyle、SpotBugsやJavaDoc結果を表示するようにしたい。下記の通り設定ファイルに追記した。

      # Generate a site.
      - run:
          name: Site
          command: mvn site
      - store_artifacts:
          path: target/site
          destination: reports

ARTIFACTSタブからファイルが参照できるようになる。

完成した設定ファイルはGitHub参照のこと。
https://github.com/hide6644/common/blob/circleci-project-setup/.circleci/config.yml