ビルドと Tomcat デプロイ

React アプリケーションを本番環境で公開するには、開発用のソースコードを ブラウザが直接実行できる形式に変換(ビルド)し、それを Web サーバーに配置する必要があります。 この章では、Vite によるビルドの仕組みから Apache Tomcat へのデプロイ方法まで、 本番運用に必要な知識をすべて解説します。

ビルドとは

開発中は Vite の開発サーバーが JSX の変換やモジュールの解決をリアルタイムで行ってくれますが、 本番環境ではそのような処理を動的に行うサーバーは存在しません。 ビルドとは、開発用のソースコードを本番環境向けに最適化された静的ファイル群に変換するプロセスです。

ビルドプロセスでは主に以下の処理が行われます。

処理説明
バンドリング (Bundling) 複数の JavaScript / CSS ファイルを少数のファイルにまとめます。これによりブラウザの HTTP リクエスト数が減り、読み込みが高速化します。
トランスパイル (Transpile) JSX を通常の JavaScript に変換し、最新の ES 構文を幅広いブラウザで動作する形式に変換します。
ミニファイ (Minification) 空白・改行・コメントを除去し、変数名を短縮してファイルサイズを削減します。
ツリーシェイキング (Tree Shaking) 実際に使用されていないコード(デッドコード)を自動的に除去します。ライブラリの一部の関数だけを import している場合、使っていない関数はバンドルに含まれません。
コード分割 (Code Splitting) アプリケーションを複数のチャンクに分割し、必要な部分だけを遅延読み込みできるようにします。
アセット処理 画像の最適化、CSS の結合・ミニファイ、フォントの処理などを行います。

Vite は内部でビルドツール Rollup を使用しています。 開発時は ESM(ES Modules)をネイティブに利用して高速な HMR(Hot Module Replacement)を実現し、 ビルド時は Rollup による最適化されたバンドルを生成します。

npm run build

ビルドは npm run build コマンドで実行します。 このコマンドは package.jsonscripts セクションに定義されている vite build を実行します。

ターミナル
# プロジェクトのルートディレクトリで実行
npm run build

ビルドが成功すると、以下のような出力が表示されます。

ビルド出力例
vite v6.x.x building for production...
✓ 142 modules transformed.
dist/index.html                     0.46 kB │ gzip:  0.30 kB
dist/assets/index-DmK4x7q2.css     1.42 kB │ gzip:  0.73 kB
dist/assets/index-CfR3lNmz.js    144.78 kB │ gzip: 46.62 kB
✓ built in 1.23s

package.jsonscripts セクションを確認してみましょう。 Vite で作成したプロジェクトにはデフォルトで build スクリプトが含まれています。

package.json(抜粋)
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}

npm run preview を実行すると、ビルド成果物をローカルでプレビューできます。 Tomcat にデプロイする前に、ビルド結果をローカルで確認するのに便利です。 デフォルトでは http://localhost:4173 で起動します。

ビルド成果物の構成

npm run build を実行すると、プロジェクトルートに dist/ ディレクトリが生成されます。 このディレクトリが Tomcat に配置する成果物のすべてです。

dist/ ディレクトリ構成
dist/
├── index.html              ← エントリポイント(SPA のメイン HTML)
├── assets/
│   ├── index-CfR3lNmz.js  ← バンドルされた JavaScript(ハッシュ付き)
│   ├── index-DmK4x7q2.css ← バンドルされた CSS(ハッシュ付き)
│   ├── vendor-Bk7a9xN4.js ← React 等のライブラリ(コード分割時)
│   └── logo-a1b2c3d4.svg  ← import した画像アセット
└── favicon.ico             ← public/ に置いたファイルはそのままコピー

各ファイルの役割を詳しく見てみましょう。

index.html

SPA のエントリポイントです。ビルドされた JavaScript と CSS への参照が自動的に挿入されています。 ブラウザはまずこのファイルを読み込み、JavaScript が実行されることでアプリケーション全体が描画されます。

dist/index.html(生成例)
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My React App</title>
    <script type="module" crossorigin src="/assets/index-CfR3lNmz.js"></script>
    <link rel="stylesheet" crossorigin href="/assets/index-DmK4x7q2.css">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

ハッシュ付きファイル名

index-CfR3lNmz.js のようにファイル名にハッシュ値が含まれています。 これはファイルの内容から計算されたハッシュで、コードが変更されるとハッシュ値も変わります。 この仕組みにより、ブラウザのキャッシュを確実に無効化(キャッシュバスティング)できます。

public/ ディレクトリのファイル

public/ ディレクトリに配置したファイル(favicon.ico, robots.txt など)は ビルド時にそのまま dist/ のルートにコピーされます。 ハッシュ化やバンドルの対象にはなりません。

vite.config.js の設定

Tomcat にデプロイする際に最も重要な設定が base オプションです。 これはビルドされたアセットの参照パス(ベースパス)を指定します。 デプロイ先の URL 構造に合わせて正しく設定する必要があります。

base の設定を誤ると、デプロイ後にページが真っ白になります。 これは最も多いデプロイ失敗の原因です。デプロイ先の URL に合わせて必ず正しく設定してください。

ROOT デプロイの場合(base: '/')

Tomcat の webapps/ROOT/ に配置する場合、アプリケーションはドメイン直下 (例: http://example.com/)でアクセスされます。 この場合は base をデフォルトの '/' のままにします。

vite.config.js(ROOT デプロイ)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: '/',  // デフォルト値なので省略可
})

サブディレクトリデプロイの場合(base: '/myapp/')

Tomcat の webapps/myapp/ に配置する場合、アプリケーションは http://example.com/myapp/ でアクセスされます。 この場合は base にコンテキストパスを設定します。

vite.config.js(サブディレクトリデプロイ)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: '/myapp/',  // Tomcat のコンテキストパスに合わせる
})

base の値は必ず スラッシュで始まりスラッシュで終わる ようにしてください。 例: '/myapp/' (正しい)、'myapp' (誤り)、'/myapp' (末尾スラッシュなしは避ける)。

base を設定すると、生成される index.html 内のアセット参照パスが変わります。

base: '/' の場合の index.html
<script src="/assets/index-CfR3lNmz.js"></script>
<link href="/assets/index-DmK4x7q2.css">
base: '/myapp/' の場合の index.html
<script src="/myapp/assets/index-CfR3lNmz.js"></script>
<link href="/myapp/assets/index-DmK4x7q2.css">

開発時とビルド時で base を切り替える

開発時は /、本番時は /myapp/ としたい場合は、 環境変数やコマンドライン引数を使って切り替えられます。

vite.config.js(環境で切り替え)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig(({ command }) => ({
  plugins: [react()],
  base: command === 'build' ? '/myapp/' : '/',
}))

Tomcat へのデプロイ手順

ビルドが完了したら、dist/ ディレクトリの中身を Tomcat に配置します。 配置方法は大きく 2 つあります。

方法 1: ROOT デプロイ

アプリケーションをドメイン直下(http://example.com/)で公開する場合は、 dist/ の中身を Tomcat の webapps/ROOT/ にコピーします。

  1. vite.config.js の base を確認

    base: '/'(デフォルト)になっていることを確認します。

  2. ビルドを実行
    ターミナル
    npm run build
  3. 既存の ROOT ディレクトリをバックアップ(任意)
    ターミナル
    # Tomcat のインストールディレクトリに移動
    cd $CATALINA_HOME
    
    # 既存の ROOT をバックアップ
    mv webapps/ROOT webapps/ROOT_backup
  4. dist/ の中身を ROOT にコピー
    ターミナル
    # ROOT ディレクトリを作成
    mkdir -p webapps/ROOT
    
    # ビルド成果物をコピー
    cp -r /path/to/your-project/dist/* webapps/ROOT/
  5. Tomcat を起動(または再起動)
    ターミナル
    # Linux / macOS
    $CATALINA_HOME/bin/startup.sh
    
    # Windows
    %CATALINA_HOME%\bin\startup.bat
  6. ブラウザで確認

    http://localhost:8080/ にアクセスして、React アプリが表示されれば成功です。

方法 2: サブディレクトリデプロイ

アプリケーションをサブディレクトリ(http://example.com/myapp/)で公開する場合は、 dist/ の中身を webapps/myapp/ にコピーします。

  1. vite.config.js の base を設定

    base: '/myapp/' を設定してからビルドします。

    vite.config.js
    export default defineConfig({
      plugins: [react()],
      base: '/myapp/',
    })
  2. ビルドを実行
    ターミナル
    npm run build
  3. dist/ の中身を myapp/ にコピー
    ターミナル
    # webapps 内にアプリ名のディレクトリを作成
    mkdir -p $CATALINA_HOME/webapps/myapp
    
    # ビルド成果物をコピー
    cp -r /path/to/your-project/dist/* $CATALINA_HOME/webapps/myapp/
  4. Tomcat を起動(または再起動)

    Tomcat が起動中であれば自動デプロイが有効な場合はディレクトリ配置だけで認識されます。

  5. ブラウザで確認

    http://localhost:8080/myapp/ にアクセスして確認します。

Tomcat の webapps ディレクトリ構造

参考として、デプロイ後の Tomcat のディレクトリ構造を示します。

webapps/ のディレクトリ構造
$CATALINA_HOME/
└── webapps/
    ├── ROOT/                  ← ドメイン直下 (http://host:8080/)
    │   ├── index.html
    │   ├── assets/
    │   │   ├── index-xxx.js
    │   │   └── index-xxx.css
    │   └── WEB-INF/           ← BrowserRouter 使用時に必要
    │       └── web.xml
    ├── myapp/                 ← サブディレクトリ (http://host:8080/myapp/)
    │   ├── index.html
    │   ├── assets/
    │   │   ├── index-xxx.js
    │   │   └── index-xxx.css
    │   └── WEB-INF/
    │       └── web.xml
    └── manager/               ← Tomcat Manager(デフォルト)

Tomcat の自動デプロイ機能(autoDeploy="true")が有効な場合、 webapps/ にディレクトリを配置するだけで自動的にアプリケーションとして認識されます。 再起動は不要です。デフォルト設定では自動デプロイが有効になっています。

HashRouter を使う場合

React Router の HashRouter を使用している場合、Tomcat 側での追加設定は不要です。 ビルドした dist/ の中身をそのまま配置するだけで動作します。

HashRouter は URL の #(ハッシュ)以降の部分でルーティングを行います。

HashRouter の URL 例
http://example.com/#/           ← ホーム
http://example.com/#/about      ← About ページ
http://example.com/#/users/123  ← ユーザー詳細ページ

ブラウザは # 以降の部分をサーバーに送信しません。 そのため、どの URL にアクセスしても Tomcat は常に index.html を返し、 クライアント側の React Router が # 以降を解析してルーティングを行います。 この仕組みにより、サーバー設定なしで SPA のルーティングが動作します。

src/App.jsx(HashRouter の例)
import { HashRouter, Routes, Route } from 'react-router-dom'

function App() {
  return (
    <HashRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </HashRouter>
  )
}

HashRouter は設定が簡単で Tomcat との相性が良いですが、URL に # が含まれるため SEO には不向きです。管理画面や社内ツールなど SEO が不要なアプリケーションに適しています。 SEO が重要な場合は次のセクションで解説する BrowserRouter を使用してください。

BrowserRouter を使う場合

BrowserRouter は通常の URL パス(http://example.com/about)でルーティングを行います。 URL が綺麗になりますが、Tomcat 側で追加の設定が必要です。

問題: ブラウザリロード時の 404 エラー

BrowserRouter を使用した SPA で /about にアクセスすると、 ブラウザは Tomcat に /about というリソースを要求します。 しかし Tomcat 上に /about というファイルやディレクトリは存在しないため、 404 エラーが返されます。

この問題を解決するには、存在しないパスへのリクエストをすべて index.html に転送し、 React Router にルーティングを任せる必要があります。

解決策 1: web.xml でエラーページをリダイレクト

最もシンプルな方法は、Tomcat の WEB-INF/web.xml で 404 エラーを index.html にリダイレクトすることです。

  1. WEB-INF ディレクトリを作成

    デプロイ先に WEB-INF ディレクトリを作成します。

    ターミナル
    # ROOT デプロイの場合
    mkdir -p $CATALINA_HOME/webapps/ROOT/WEB-INF
    
    # サブディレクトリデプロイの場合
    mkdir -p $CATALINA_HOME/webapps/myapp/WEB-INF
  2. web.xml を作成

    以下の内容で WEB-INF/web.xml を作成します。

    WEB-INF/web.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                                 https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
             version="6.0">
    
      <error-page>
        <error-code>404</error-code>
        <location>/index.html</location>
      </error-page>
    
    </web-app>
  3. 動作を確認

    Tomcat を再起動し、/about などのルートに直接アクセスしてページが正常に表示されることを確認します。

この方法の仕組み:

  1. ブラウザが /about をリクエスト
  2. Tomcat は /about に対応するファイルが見つからず 404 エラーを生成
  3. web.xmlerror-page 設定により、index.html が返される
  4. ブラウザが index.html を読み込み、JavaScript が実行される
  5. React Router がブラウザの URL(/about)を解析し、対応するコンポーネントを表示

この方法では HTTP ステータスコードが 404 のまま index.html の内容が返されます。 通常のブラウザ利用では問題ありませんが、ステータスコード 200 を返したい場合は 次の Servlet フィルター方式を使用してください。

解決策 2: Servlet フィルターで URL リライト

より正確な制御が必要な場合は、カスタム Servlet フィルターを使って URL リライトを行います。 この方法では、静的ファイルへのリクエストはそのまま処理し、 それ以外のリクエストのみ index.html にフォワードします。 ステータスコードも正常な 200 が返されます。

SpaRedirectFilter.java
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;

public class SpaRedirectFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        String path = req.getRequestURI();

        // 静的ファイル(拡張子あり)やAPIリクエストはそのまま処理
        if (path.contains(".") || path.startsWith("/api/")) {
            chain.doFilter(request, response);
            return;
        }

        // それ以外は index.html にフォワード
        request.getRequestDispatcher("/index.html")
               .forward(request, response);
    }
}

フィルターを web.xml に登録します。

WEB-INF/web.xml(フィルター方式)
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                             https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

  <filter>
    <filter-name>SpaRedirectFilter</filter-name>
    <filter-class>com.example.SpaRedirectFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>SpaRedirectFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>

Servlet フィルター方式を使う場合、コンパイル済みの .class ファイルを WEB-INF/classes/com/example/ ディレクトリに配置する必要があります。 あるいは、JAR ファイルとしてパッケージし WEB-INF/lib/ に配置します。 シンプルな SPA 配信のみが目的であれば、解決策 1 の error-page 方式で十分です。

BrowserRouter の basename 設定

サブディレクトリにデプロイする場合、BrowserRouter にも basename を設定する必要があります。

src/App.jsx(basename の設定)
import { BrowserRouter, Routes, Route } from 'react-router-dom'

function App() {
  return (
    <BrowserRouter basename="/myapp">
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  )
}

サブディレクトリデプロイ時は、3 つの設定をすべて揃える必要があります。

  1. vite.config.jsbase: '/myapp/'
  2. BrowserRouterbasename="/myapp"
  3. Tomcat の webapps/myapp/ ディレクトリ名

これらが一致しないと、ルーティングやアセットの読み込みが正しく動作しません。

context.xml の設定

必要に応じて、Tomcat の context.xml でアプリケーション固有の設定を行えます。 META-INF/context.xml をデプロイディレクトリに配置します。

META-INF/context.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context reloadable="false">
  <!-- 静的ファイルのキャッシュ設定(10日間) -->
  <Resources cachingAllowed="true"
             cacheMaxSize="100000"
             cacheTtl="864000000" />
</Context>

主要な設定項目を確認しましょう。

属性説明推奨値
reloadable クラスファイルの変更を監視して自動再読み込みするか false(静的ファイルのみなので不要)
cachingAllowed 静的リソースのキャッシュを有効にするか true
cacheMaxSize キャッシュの最大サイズ(KB) 100000(約 100 MB)

React SPA は基本的に静的ファイルの配信のみなので、context.xml の設定は 最小限で問題ありません。特に要件がなければ省略しても構いません。

環境変数の管理

Vite では .env ファイルを使って環境変数を管理できます。 VITE_ プレフィックスが付いた変数のみがクライアントサイドのコードに公開されます。

セキュリティ上の注意: VITE_ プレフィックスが付いた変数は ビルド後の JavaScript に平文で埋め込まれます。 API キーやパスワードなどの秘密情報は絶対に VITE_ 変数に設定しないでください。

.env ファイルの種類

ファイル名読み込みタイミング用途
.env 常に読み込み 全環境共通のデフォルト値
.env.local 常に読み込み(.gitignore に追加推奨) ローカル固有の設定
.env.development npm run dev 開発環境の設定
.env.production npm run build 本番環境の設定

実践例: API エンドポイントの切り替え

.env.development
# 開発環境 — Vite の開発サーバーのプロキシを使用
VITE_API_BASE_URL=http://localhost:5173/api
.env.production
# 本番環境 — Tomcat 上の API サーバー
VITE_API_BASE_URL=https://api.example.com

アプリケーションコード内では import.meta.env でアクセスします。

src/api/client.js
const API_BASE = import.meta.env.VITE_API_BASE_URL;

export async function fetchUsers() {
  const response = await fetch(`${API_BASE}/users`);
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  return response.json();
}

ビルド時に .env.production の値が JavaScript に直接埋め込まれます。 例えば import.meta.env.VITE_API_BASE_URL は ビルド後のコードでは "https://api.example.com" という文字列に置換されます。 実行時に動的に変更することはできないため、環境ごとに別々にビルドする必要があります。

キャッシュ対策

Web アプリケーションを更新した際に、ユーザーのブラウザが古いキャッシュを使い続ける問題は よくあるトラブルです。Vite はこの問題をコンテンツハッシュによって自動的に解決します。

コンテンツハッシュの仕組み

Vite が生成するファイル名にはハッシュ値が含まれています。

ファイル名の例
# v1.0(初回ビルド)
assets/index-CfR3lNmz.js
assets/index-DmK4x7q2.css

# v1.1(コードを変更して再ビルド)
assets/index-Ax8bPq1K.js    ← ハッシュが変わる
assets/index-DmK4x7q2.css   ← CSS は変更なしなので同じハッシュ

ファイルの内容が変わるとハッシュ値も変わるため、ブラウザは新しいファイルとして認識し、 自動的に最新版をダウンロードします。内容が変わっていないファイルはハッシュが同じなので、 キャッシュがそのまま利用され、無駄なダウンロードが発生しません。

index.html のキャッシュ制御

index.html 自体はハッシュ化されないため、ブラウザがキャッシュする可能性があります。 Tomcat の web.xml でキャッシュヘッダーを設定することで対策できます。

WEB-INF/web.xml(キャッシュヘッダー追加)
<web-app ...>

  <!-- BrowserRouter 用の 404 フォールバック -->
  <error-page>
    <error-code>404</error-code>
    <location>/index.html</location>
  </error-page>

  <!-- index.html のキャッシュを無効化 -->
  <filter>
    <filter-name>NoCacheFilter</filter-name>
    <filter-class>org.apache.catalina.filters.ExpiresFilter</filter-class>
    <init-param>
      <param-name>ExpiresDefault</param-name>
      <param-value>access plus 0 seconds</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>NoCacheFilter</filter-name>
    <url-pattern>/index.html</url-pattern>
  </filter-mapping>

  <!-- アセットファイルに長期キャッシュを設定 -->
  <filter>
    <filter-name>AssetsCacheFilter</filter-name>
    <filter-class>org.apache.catalina.filters.ExpiresFilter</filter-class>
    <init-param>
      <param-name>ExpiresDefault</param-name>
      <param-value>access plus 1 year</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>AssetsCacheFilter</filter-name>
    <url-pattern>/assets/*</url-pattern>
  </filter-mapping>

</web-app>

index.html はキャッシュ無効化(access plus 0 seconds)、 assets/ 配下のハッシュ付きファイルは長期キャッシュ(access plus 1 year) が最適な設定です。ハッシュ付きファイルは内容が変われば名前が変わるため、 長期キャッシュでも古いファイルが使われる心配がありません。

デプロイの自動化

手動でのコピー作業は手間がかかるうえミスも起こりやすいため、 シェルスクリプトやCI/CDパイプラインでデプロイを自動化することをおすすめします。

シェルスクリプトによる自動デプロイ

deploy.sh
#!/bin/bash
# React アプリを Tomcat にデプロイするスクリプト
# 使い方: ./deploy.sh [root|myapp]

set -e  # エラー時に即座に終了

# === 設定 ===
CATALINA_HOME="/opt/tomcat"
PROJECT_DIR="/home/user/my-react-app"
DEPLOY_TARGET="${1:-ROOT}"  # デフォルトは ROOT デプロイ

# === ビルド ===
echo "=== ビルドを開始します ==="
cd "$PROJECT_DIR"
npm ci          # クリーンインストール
npm run build   # プロダクションビルド

if [ ! -d "dist" ]; then
  echo "エラー: dist ディレクトリが見つかりません"
  exit 1
fi

# === デプロイ ===
DEPLOY_DIR="$CATALINA_HOME/webapps/$DEPLOY_TARGET"

echo "=== デプロイ先: $DEPLOY_DIR ==="

# 既存のデプロイを日付付きでバックアップ
if [ -d "$DEPLOY_DIR" ]; then
  BACKUP_DIR="${DEPLOY_DIR}_backup_$(date +%Y%m%d_%H%M%S)"
  echo "バックアップ: $BACKUP_DIR"
  mv "$DEPLOY_DIR" "$BACKUP_DIR"
fi

# ビルド成果物をコピー
mkdir -p "$DEPLOY_DIR"
cp -r dist/* "$DEPLOY_DIR/"

# WEB-INF/web.xml をコピー(BrowserRouter 使用時)
if [ -f "tomcat/web.xml" ]; then
  mkdir -p "$DEPLOY_DIR/WEB-INF"
  cp tomcat/web.xml "$DEPLOY_DIR/WEB-INF/"
  echo "WEB-INF/web.xml をコピーしました"
fi

echo "=== デプロイが完了しました ==="
echo "URL: http://localhost:8080/$( [ \"$DEPLOY_TARGET\" = \"ROOT\" ] && echo '' || echo \"$DEPLOY_TARGET/\" )"
ターミナル
# 実行権限を付与
chmod +x deploy.sh

# ROOT にデプロイ
./deploy.sh

# myapp にデプロイ
./deploy.sh myapp

CI/CD での自動デプロイ

GitHub Actions や Jenkins などの CI/CD ツールを使えば、 Git への push をトリガーにビルドとデプロイを完全に自動化できます。 基本的な流れは以下の通りです。

  1. ソースコードを Git リポジトリに push
  2. CI/CD パイプラインが起動
  3. npm ci で依存関係をインストール
  4. npm run build でビルド
  5. npm test でテストを実行
  6. テスト成功後、ビルド成果物を Tomcat サーバーにデプロイ(SCP / rsync / Docker など)

本番デプロイ前に npm run preview でビルド成果物をローカルで確認する習慣をつけましょう。 ビルド時にのみ発生する問題(環境変数の未設定、base パスの誤りなど)を事前に発見できます。

トラブルシューティング

Tomcat へのデプロイでよく遭遇する問題と解決策をまとめます。

症状原因解決策
ページが真っ白 vite.config.jsbase がデプロイ先と一致していない デプロイ先のコンテキストパスに合わせて base を修正し、再ビルドする
リロードすると 404 BrowserRouter を使用しているが、サーバー側のフォールバック設定がない WEB-INF/web.xmlerror-page を設定するか、HashRouter に切り替える
CSS / JS が読み込めない アセットの参照パスが間違っている(base の設定ミス) ブラウザの開発者ツール(Network タブ)で実際のリクエストパスを確認し、base を修正する
画像が表示されない public/ 内の画像パスが base を考慮していない 絶対パスの場合は base を含めるか、import を使って画像を参照する
API で CORS エラー フロントエンドとバックエンドのオリジン(ドメイン・ポート)が異なる バックエンド側で CORS ヘッダーを設定するか、Tomcat のリバースプロキシを設定する
環境変数が undefined VITE_ プレフィックスが付いていない、または .env.production がない 変数名が VITE_ で始まっていることを確認し、.env.production を作成する
古いバージョンが表示される ブラウザが index.html をキャッシュしている Ctrl+Shift+R で強制リロード、またはキャッシュヘッダーを設定する
Tomcat 起動時にエラー web.xml の記述ミス(タグの閉じ忘れ、名前空間の誤りなど) catalina.out のログを確認し、web.xml の構文を検証する

問題が発生したら、まずブラウザの開発者ツールを開いてください。 Console タブでエラーメッセージ、Network タブで失敗しているリクエスト(赤字の行)を確認すると、 原因の特定が大幅に早くなります。

本番環境のチェックリスト

本番環境にデプロイする前に、以下のチェックリストを確認しましょう。

カテゴリチェック項目説明
ビルド 本番ビルドを使用している npm run build で生成した dist/ を使用していること。開発サーバーをそのまま公開しないこと。
ビルド base パスが正しい vite.config.jsbase がデプロイ先のコンテキストパスと一致していること。
ビルド 環境変数が本番用に設定されている .env.production で API エンドポイント等が本番環境向けになっていること。
ルーティング SPA フォールバックが設定されている BrowserRouter 使用時は web.xml で 404 フォールバックを設定していること。
セキュリティ HTTPS が有効 SSL/TLS 証明書を設定し、HTTPS でのアクセスを強制していること。
セキュリティ セキュリティヘッダーが設定されている X-Content-Type-OptionsX-Frame-OptionsContent-Security-Policy 等を設定していること。
セキュリティ 機密情報がコードに含まれていない API キーやパスワードが JavaScript バンドルに埋め込まれていないこと。
パフォーマンス Gzip / Brotli 圧縮が有効 Tomcat の server.xmlcompression="on" を設定し、転送サイズを削減していること。
パフォーマンス キャッシュヘッダーが適切 index.html はキャッシュ無効化、assets/ は長期キャッシュが設定されていること。
パフォーマンス バンドルサイズが適切 npm run build の出力でファイルサイズを確認し、不要な依存関係がないこと。
運用 エラーページが設定されている 500 エラー等の場合にユーザーフレンドリーなページが表示されること。
運用 ログ設定が適切 Tomcat のアクセスログが有効になっており、問題の追跡が可能なこと。
運用 バックアップ・ロールバック手順がある デプロイに問題があった場合に、前のバージョンに素早く戻せる手順があること。

Gzip 圧縮の設定

Tomcat で Gzip 圧縮を有効にするには、server.xml の Connector 設定に compression 属性を追加します。

$CATALINA_HOME/conf/server.xml(抜粋)
<Connector port="8080"
           protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           compression="on"
           compressionMinSize="1024"
           compressibleMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json" />

Gzip 圧縮により、JavaScript と CSS のファイルサイズが通常 60-70% 削減されます。 ビルド出力に表示される gzip の数値が、圧縮後の実際の転送サイズの目安です。

まとめ

React アプリを Tomcat にデプロイする際の全体的な流れを振り返りましょう。

  1. vite.config.jsbase を正しく設定する
  2. .env.production で本番用の環境変数を設定する
  3. npm run build でビルドする
  4. dist/ の中身を Tomcat の webapps/ にコピーする
  5. BrowserRouter 使用時は WEB-INF/web.xml を設置する
  6. ブラウザで動作を確認する

適切に設定すれば、React SPA と Tomcat の組み合わせは安定した本番環境を構築できます。 トラブルが発生した場合は、このページのトラブルシューティングセクションとチェックリストを参照してください。