Laravel + React Starter Kit を Docker で検証して改良した fork を公開
公式の laravel/react-starter-kit(Inertia + React 19 + shadcn/ui + Fortify)を Docker Desktop で実際に動かし、本番運用に耐える形に書き直したフォークを公開する。ドキュメント不足 2 件と Laravel backend 側の実コード改良 6 件で合計 8 commits、テストは公式版の 40 passed をベースに 44 passed / 151 assertions まで増加。以下は検証…
検証環境
- PHP 8.5.5
- Laravel 13.5.0
- Composer 2.9.7
- Node 22.22.2
- npm 10.9.7
- Database SQLite (tests)
- OS Docker Desktop (php:8.5-cli-bookworm)
Laravel + React Starter Kit を Docker で検証して改良した fork を公開
公式の laravel/react-starter-kit(Inertia + React 19 + shadcn/ui + Fortify)を Docker Desktop で実際に動かし、本番運用に耐える形に書き直したフォークを公開する。ドキュメント不足 2 件と Laravel backend 側の実コード改良 6 件で合計 8 commits、テストは公式版の 40 passed をベースに 44 passed / 151 assertions まで増加。以下は検証手順・finding ごとの根拠・最終 diff の全体像を記録した作業ログ兼リファレンス。
姉妹記事:
- Laravel + Vue Starter Kit を Docker で検証して改良した fork を公開(backend が共通なので findings も同一)
- Laravel + Livewire Starter Kit を Docker で検証して改良した fork を公開(fronent 構造が違うので findings が一部変わる)
対象の公式サンプル
| 項目 | 値 |
|---|---|
| 名前 | Laravel + React Starter Kit |
| 公式 URL | https://github.com/laravel/react-starter-kit |
| 概要 | Laravel 13 + Inertia + React 19 + shadcn/ui + Fortify 認証を含むスターター |
| 元ライセンス | MIT |
| 改良版ライセンス | MIT |
検証日
2026-04-19
検証環境
検証はすべて Docker Desktop 上のコンテナ内で実施した。本体サイトが稼働している Xserver 上や、開発マシンに直接 PHP / Node を入れる方法は採らない。
| 項目 | 値 |
|---|---|
| 基盤イメージ | php:8.5-cli-bookworm + Node 22 |
| PHP | 8.5.5 |
| Laravel Framework | 13.5.0 |
| Composer | 2.9.7 |
| Node / npm | 22.22.2 / 10.9.7 |
| テスト用 DB | SQLite |
| 上流 commit | laravel/react-starter-kit@2e22614 |
再現手順は改良版ブランチの codelift/Dockerfile + codelift/docker-compose.yml に同梱した。docker compose run --rm app 一発で、Composer install → .env 生成 → SQLite migrate → npm install → npm run build → php artisan test まで走る(詳細手順は記事末尾の「再現と取り込み」に記載)。
公式版の評価
公式版 main をそのまま Docker 上で php artisan test にかけた結果は 40 passed (136 assertions)、20.51s。Vite ビルドも正常終了し、テンプレートとしての正当性は確保されている。
そのうえで、production 投入前に触っておきたい設定が 8 件残っている。
改良方針と具体的な改善点
A. composer install 前に npm run build するとビルドが失敗する
症状: 真っさらな clone 直後に npm run build を先に走らせると、Vite ビルドが次のエラーで落ちる。
require(.../vendor/autoload.php): Failed to open stream: No such file or directory
in artisan on line 10
原因: @laravel/vite-plugin-wayfinder が Vite ビルド中に php artisan wayfinder:generate を shell out するため、Composer 依存が未インストールだと artisan が起動できない。
メッセージに composer install の文字が一切出ないので、初見の contributor は Vite か Wayfinder のバグを疑って時間を溶かしやすい。
改良: README に Setup セクションを追加し、composer install が npm install より先に来ることを明示した。
B. .env.example が開発用デフォルトだけで production 値のヒントが無い
症状: 公式の .env.example は APP_ENV=local / APP_DEBUG=true / LOG_LEVEL=debug / SESSION_ENCRYPT=false、SESSION_SECURE_COOKIE は未設定。
開発向けとしては妥当だが、production 差分を把握せずにコピーするとデバッグモードのまま本番稼働になる。
改良: .env.example の該当行にインラインコメントで production 推奨値を併記した(ランタイムには影響しない、純粋なドキュメント改善)。
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
# Production overrides:
# APP_ENV=production
# APP_DEBUG=false
# APP_URL must be the https:// URL that TLS terminates at.
C. config/app.php の timezone が文字列リテラル
症状: config/app.php:68 が 'timezone' => 'UTC' とハードコード。.env に APP_TIMEZONE=Asia/Tokyo を書いても効かない。
Laravel 本体の最新 skeleton は env('APP_TIMEZONE', 'UTC') に移行済みで、starter kit 側の追従漏れ。
改良: 1 行修正。
- 'timezone' => 'UTC',
+ 'timezone' => env('APP_TIMEZONE', 'UTC'),
.env.example にも APP_TIMEZONE を候補として記載した。
D. セキュリティレスポンスヘッダの middleware が無い
症状: bootstrap/app.php に登録されている web middleware は HandleAppearance / HandleInertiaRequests / AddLinkHeadersForPreloadedAssets の 3 つだけ。CSP / HSTS / X-Content-Type-Options / X-Frame-Options / Referrer-Policy / Permissions-Policy のいずれも出ていない。
本番環境では reverse proxy 側で設定することが多いが、Laravel 単体で nginx + php-fpm に乗せるケースでは何も付かないまま公開されることになる。
改良: app/Http/Middleware/SetSecurityHeaders.php を新規作成し、web stack の末尾に登録。環境に応じて付与するヘッダを分けた。
- 全環境共通:
X-Content-Type-Options: nosniff/X-Frame-Options: SAMEORIGIN/Referrer-Policy: strict-origin-when-cross-origin/Permissions-Policy: camera=(), microphone=(), geolocation=() - 本番のみ:
Strict-Transport-Security: max-age=31536000; includeSubDomains/Content-Security-Policy(下記)
CSP は Inertia + Vite ビルド後アセットを想定した保守的な設定にした:
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self';
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none'
'unsafe-inline' を許可しているのは、Inertia が初期 props を inline script として埋める実装になっているため。将来的には nonce-based に切り替える余地がある。
HSTS と CSP をローカル・テストで付けない理由は、(1) HSTS キャッシュが localhost に入ると後で http に戻せなくなる、(2) CSP が Vite HMR を阻害しやすい、の 2 点。
テストは tests/Feature/SecurityHeadersTest.php を新規追加し、常時 on のヘッダが付くこと / production 限定のヘッダが testing 環境では付かないことを 2 件で検証した。
E. HTTPS scheme を強制するヘルパーが無い
症状: リポジトリ全域を forceScheme / forceHttps で grep しても 0 件ヒットしない。TLS を proxy で終端して backend には http:// が届く構成では、Laravel が生成する URL(route ヘルパー、asset URL、redirect 先)が http:// になり、mixed-scheme レスポンスや無限 redirect が発生しうる。
改良: AppServiceProvider::configureDefaults に 3 行追加。
if (app()->isProduction()) {
URL::forceScheme('https');
}
trusted proxies の設定(TrustProxies middleware の CIDR 指定)はインフラ構成依存なのでこの commit では触らない。該当文言は commit メッセージに残した。
F. パスワード強度ルール → 前回の検証ログが誤読。既対応
前回の検証ログ(このフォークの最初の commit)では、PasswordValidationRules::passwordRules() の中で使われる Password::default() が「カスタマイズされていない」と判定していた。これは誤りだった。
app/Providers/AppServiceProvider.php 内で既に以下が設定されている:
Password::defaults(fn (): ?Password => app()->isProduction()
? Password::min(12)->mixedCase()->letters()->numbers()->symbols()->uncompromised()
: null,
);
Password::defaults()(複数形、setter)で closure を登録すると、Password::default()(単数形、getter)呼び出し時にそれが解決される。したがって production では 12 文字以上・大小英数記号必須・HIBP 照会付きのポリシーが有効。
変更なし。再監査で気付いたため、この記事では正直に「誤検知だった」と記録する。
G. ログインのレート制限が email + IP だけ
症状: FortifyServiceProvider::configureRateLimiting は次の 1 段のみ。
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
単一 IP からの brute force には十分だが、プロキシや bot net で IP をローテートする攻撃者はキーが毎回変わるので、単一アカウントに対して 1 時間あたり数千回の試行が実質無制限にできる。
改良: Laravel の RateLimiter::for は array を返せる(全 limit が個別に評価され、どれか一つにでも触れると 429 になる)ので、2 段構えにした。
return [
// Burst guard: 5 attempts / minute per email + IP (unchanged).
Limit::perMinute(5)->by($email.'|'.$ip),
// Account-level cumulative guard: 20 attempts / hour per email,
// independent of IP.
Limit::perHour(20)->by('login-account|'.$email),
];
既存の test_users_are_rate_limited テストは前者(per-minute)を対象にしているのでそのまま pass する。後者(per-hour)の挙動検証は Laravel 内部の key-hashing に依存するため、この commit では専用テストを追加しなかった(fragile になるため)。
H. 2FA は機能として存在するが operator が強制するための入口が無い
観測: config/fortify.php:148-152 で Features::twoFactorAuthentication が有効化され、UI / 設定画面 / SecurityTest は揃っている。ユーザは opt-in で設定できる。
一方で次が欠けている:
- 管理者アカウントに対して 2FA 必須化するポリシーヘルパー
- 未設定ユーザに対するリマインダー・nag バナー
- ロール別に 2FA 強制を切り替える仕組み
これは defect というより 運用設計の判断。コードに載せるべきか、operator が自前で決めるべきか、ケースバイケース。今回の改良 commits には含めず、記事では議論項目として置く。
I. ログチャンネルがサブシステム別に分かれていない
症状: config/logging.php は default stack → single の構成で、login / failed login / password reset / 2FA enable・disable イベントもアプリログ・DB クエリログと同じ laravel.log に interleave する。インシデント対応で「認証関連だけ追いたい」時に grep 前提になる。
改良: 2 ファイルを変更。
config/logging.phpにauthチャンネルを追加(daily driver、90 日保持、LOG_AUTH_LEVEL/LOG_AUTH_DAYSで env 可変)app/Listeners/AuthActivitySubscriber.phpを新規作成し、AppServiceProvider::bootでEvent::subscribe登録
subscriber は次のイベントを auth チャンネルに info で書き出す:
Illuminate\Auth\Events\Registered/Login/Logout/Failed/PasswordResetLaravel\Fortify\Events\TwoFactorAuthenticationEnabled/TwoFactorAuthenticationDisabled
各レコードには user_id / email / ip / user_agent の構造化コンテキストを含める。storage/logs/auth-YYYY-MM-DD.log に出るので、監査時に tail -f / grep が単純に通る。
テストは tests/Feature/Auth/AuthLoggingTest.php を追加し、成功ログイン / 失敗ログインの 2 パスを検証した。
J. 設定画面のレート制限が password endpoint だけ
症状: routes/settings.php で throttle を明示しているのは settings/password のみ。profile update(PATCH)と profile destroy(DELETE)はグローバルデフォルトに依存している。
セッションが乗っ取られた場合、profile の連続書き換えや account 連続削除試行は global デフォルトの範囲で可能。
改良:
- Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update');
+ Route::patch('settings/profile', [ProfileController::class, 'update'])
+ ->middleware('throttle:10,1')
+ ->name('profile.update');
- Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
+ Route::delete('settings/profile', [ProfileController::class, 'destroy'])
+ ->middleware('throttle:3,1')
+ ->name('profile.destroy');
account 削除は terminal な操作なので厳しめの 3/min に絞った。通常ユーザの正常操作で触れる閾値ではないはず。
before / after 比較
| 観点 | 公式版 | 改良版 |
|---|---|---|
php artisan test |
40 passed / 136 assertions | 44 passed / 151 assertions |
| Setup 順序のドキュメント | なし | README に明記 |
.env.example に production 値の注意 |
なし | インラインコメントで併記 |
APP_TIMEZONE を env から設定 |
不可(ハードコード) | 可 |
| 基本 security headers | 一切無し | 常時 4 種 + 本番のみ HSTS / CSP |
| HTTPS scheme 強制 | なし | 本番のみ URL::forceScheme('https') |
| ログインのレート制限 | email+IP の 1 段 | email+IP + email 単体の 2 段 |
| 認証イベントのログ | 他ログと混在 | 専用 auth daily channel(90 日) |
| settings endpoints のレート制限 | password のみ | profile update / destroy にも |
どのケースでこの改良版が向くか
laravel/react-starter-kitをテンプレとして本番プロダクトを立ち上げる予定で、セキュリティと観測性の足回りを最初に固めてから機能開発に入りたい- 既にスキャフォールド済みのプロジェクトに、production hardening チェックリストとして各 commit を cherry-pick したい
- 監査ログや CSP ヘッダが要件として出ている開発現場で、Laravel 本体側に議論を持ち込む前にスターター側で閉じたい
逆に「すぐ捨てる PoC」「1 〜 2 週間だけ動けばいいデモ」には過剰かもしれない。その場合でも A / B / C だけ取り込んでおくと後が楽。
FAQ
Q. なぜ CSP を local / test では付けない?
ローカルで Vite HMR を使う時に CSP の connect-src / script-src で引っかかりやすいため。また、HSTS は一度 localhost が強制 HTTPS キャッシュに入ると、後で http に戻したいときに手元ブラウザ側で明示的に解除するまで届かなくなる。いずれも production 限定に絞った方が開発体験を壊さない。
Q. 'unsafe-inline' を許可したのは妥協?
はい。Inertia が初期 props を <script> inline で出力するため、現状これを外すと何も動かない。本来は nonce-based CSP に切り替えるべきで、その際は HandleInertiaRequests 側で nonce を払い出して Inertia 側に渡す改修が要る。今回はスコープ外。
Q. Finding F を 1 つ削除して正直に書いたのはなぜ?
CodeLift の方針として「検証していない内容を断定しない」を置いている。前回の検証が単純な誤読だったので、改良対象から外すだけでなく、誤読だった事実も記事に残すのが筋だと判断した。読者が同じ読み方をするのを防ぐ意味もある。
Q. fork を PR として本家に出さないのか?
一部は PR 候補として十分 筋がある(A の README note, C の timezone env 化など)。ただし D の CSP や I の auth channel は「どこまで本体に含めるか」という設計議論になりやすく、starter kit のスコープから外れる可能性もある。本記事ではフォーク保持を基本とし、PR 化は個別に判断する。
再現と取り込み
改良版は improvements ブランチ に積んである。自分の手元で走らせたい場合:
git clone https://github.com/codelift-dev/react-starter-kit.git
cd react-starter-kit
git checkout improvements
docker compose -f codelift/docker-compose.yml build
docker compose -f codelift/docker-compose.yml run --rm app
公式版との差分(アプリ本体のみ、CodeLift 検証ファイル除く)を見たい場合:
git diff main improvements -- . ':!codelift'
個別の改良だけ取り込みたい場合は、各 commit が独立しているので cherry-pick 可能。
ライセンス / 法務
- 元サンプル: MIT(Laravel LLC / 開発チーム)
- 改良版: MIT(CodeLift / JIT 株式会社)
fork の運用方針は CodeLift の 運営仕様 に準拠する。本記事内の検証結果は検証日時点のものであり、以降の upstream 更新により状況は変わる可能性がある。
この記事を含む比較
関連記事
- Laravel + Vue Starter Kit を Docker で検証して改良した fork を公開 公式の laravel/vue-starter-kit(Inertia + Vue 3 + shadcn-vue + Fortify)を Docker で動かし、本番運用に耐える形に書き直したフォークを公開する。結果は React 版と完全対応の 8 commits(Laravel backend が両 Starter で共通なので findings も同一)、テストは 40 passed → 44 passed / 151 assertions。Vue 独自に知りたい人が…
- Laravel + Livewire Starter Kit を Docker で検証して改良した fork を公開 公式の laravel/livewire-starter-kit(Livewire v4 + Flux + Alpine)を Docker Desktop で動かし、本番運用に耐える形に書き直したフォークを公開する。テストは公式版の 33 passed をベースに 37 passed / 92 assertions まで増加、改良は 7 commits。React / Vue 版とは findings の構成が違う点が Livewire 変種の特徴で、本記事はその差分を中心…