Cloud Run へのデプロイが「PORT で待ち受けられなかった」で落ちる。PORT の設定を見直しても変わらず、Cloud Logging にはアプリのログが1行も無い。これは本当にポートの問題だろうか。疑うべきは、アプリが動き出す前に読み込まれる依存だ。
筆者が運営する SaaS では、この状態が約6週間続いた。デプロイは失敗続きなのに、本番は何事もなく動いていた。新しいリビジョンが起動しないので、古いリビジョンが配信を続けていたからだ。依存の更新後に起動が全滅したとき、AI コーディングエージェントに何を調べさせ、何を確かめさせるかを以下にまとめる。
エラー文はポートを指すが、落ちているのはその手前
デプロイのエラーはこうだった。
The user-provided container failed to start and listen on the port defined provided by the PORT=3000 environment variable
公式のトラブルシューティングも、手元でイメージを動かす確認の次にポートと待ち受けアドレスを挙げ、ログを見るのは最後の項目だ。エラー文どおりならポートの問題のはずが、失敗したリビジョンのログは実際はこうだった。
◇ injected env (0) from .env
ERROR Uncaught signal: 11, pid=1, tid=1
WARNING Container terminated on signal 11.
.env を読んだ直後、アプリの起動ログより前に signal 11(SIGSEGV、ネイティブコードのメモリ違反)で落ちている。JavaScript の例外ならスタックトレースが出るので、何も出ずに消えたなら、モジュールの読み込み中に C/C++ のアドオンが落ちたことをまず疑う。この backend のネイティブアドオンはパスワードハッシュの argon2 だけで、8月初めの依存更新で 0.44 から 0.45 に上がっていた。
argon2 0.45.0 のリリースノートにはこうある。
Drops support for Node 18 and 20 (probably still works, but isn't tested)
本番のイメージは node:20-alpine だった。警告にも故障にも気づけたはずの場所が5つあり、どれも素通りした。
| 気づけたはずの場所 | 実際に起きたこと |
|---|---|
| 依存更新の PR | 約40パッケージをまとめた non-major の更新で、argon2 は「minor」の1行。PR 本文のリリースノートは先頭のパッケージの途中で切り詰められ、argon2 の注記は載っていなかった |
| package.json の engines | 0.45.1 でも node >=16.17.0 のまま。Node 20 で npm install しても警告は出ない |
| イメージのビルド | 別の理由で --ignore-scripts を付けて install していた。スクリプトを走らせると Node 20 ではソースからのビルドに進み、ビルドツールの無いイメージでは install で落ちる(筆者の手元の arm64 で確認)。その失敗が起動時まで先送りされた |
| CI のテスト | ubuntu(glibc)の x64 で走る。argon2 は prebuilt を OS・CPU・libc ごとに別ファイルで同梱していて、CI と本番では読み込む .node ファイルが違う |
| 本番の監視 | 起動しないリビジョンにはトラフィックが向かない。古いリビジョンが配信を続けるので、障害として見えない |
0.x の版では、minor の更新でも互換性が切れることがある。engines を見て「Node 20 で動く」と判断するエージェントは、この件では間違える。
切り分けをエージェントに任せる指示
エージェントには「直して」の前に「再現して表にして」と頼む。先に直させると、PORT の設定やヘルスチェックの猶予など、エラー文に沿った修正から手を付けやすい。
Cloud Run のデプロイが起動失敗で落ちています。まだ何も直さず、次を調べて報告してください。
1. 失敗したリビジョンのログに、アプリ自身の最初のログ行が出ているか。出ていなければ終了シグナルを書く
2. デプロイの履歴を、実際にコンテナをデプロイした run と、変更が無く skip された run に分ける。
最後に成功したデプロイと、最初に失敗したデプロイの間のコミットを挙げる
3. その間の lockfile の差分から、ネイティブアドオン(.node を含むパッケージ)の版の変化を列挙する。
各パッケージのリリースノートで、対応する Node の版の変更を確かめる(engines だけで判断しない)
4. 本番と同じ条件で再現する。ベースイメージ、apk / apt で入れるパッケージ、install のフラグ
(--ignore-scripts の有無)、CPU(Cloud Run は linux/amd64)をそろえ、アドオンを require するだけのコマンドを実行する
5. Node のメジャー版とアドオンの版を1つずつ入れ替え、組み合わせごとの終了コードを表にする
2 で skip された run を分けるのは、変更の無い run の「成功」が混ざると、壊れた時期を遅く読み違えるからだ。4 の条件は1つも省かせない。--ignore-scripts の有無が違うだけで、再現環境は install で落ち、本番とは別の失敗を追うことになる。
手元で試すなら、再現のコマンドはこの形になる。
docker run --rm node:20-alpine sh -c '
apk add --no-cache libc6-compat >/dev/null
cd /tmp && npm init -y >/dev/null
npm install argon2@0.45.1 --ignore-scripts >/dev/null 2>&1
node -e "require(\"argon2\")" && echo OK
'; echo "exit=$?"
上のコマンドは arm64 のマシンでもそのまま動くが、CPU はそろっていない。Cloud Run と同じ CPU で確かめるには --platform linux/amd64 を足す。arm64 のマシンでは、amd64 のエミュレーションを有効にしておく必要がある。
総当たりの結果
Cloud Run 上の実績と、手元で上のコマンドの版を入れ替えた結果を並べる。
| Node | argon2 | Cloud Run(linux/amd64) | 筆者の手元(linux/arm64) |
|---|---|---|---|
| 20(alpine) | 0.45.1 | signal 11 で起動失敗 | exit 139(128+11。signal 11 と同じ) |
| 20(alpine) | 0.44.0 | 8月まではこの組み合わせで稼働 | OK |
| 22(alpine) | 0.45.1 | 直したリビジョンが起動し、ヘルスチェックが 200 | OK |
手元では require だけで落ちた。arm64 では glibc の node:20-slim でも exit 139 になるのに、x64 の glibc で走る CI は通っている。CPU が変われば結果も変わるので、手元の arm64 の列は本番の答えの代わりにならない。4 で CPU までそろえさせるのはこのためで、「musl だから落ちる」とも言い切れない。変数を1つずつ動かさないと、たまたま揃っていた条件を原因と取り違える。
直し方を選ぶ
Node 20 は2026年4月30日にサポートが終わっているので、Node を 22 以上に上げるほうを選ぶ。argon2 を 0.44 に固定すると、次の依存更新でまた同じ PR が来る。
再発を止める
自動化するのは次の2つ。
- CI で、ビルドした本番イメージを起動してヘルスチェックを叩く。少なくとも、イメージの中でネイティブアドオンを require させる。依存更新の PR をマージする前に必ず通る checks に入れる
- デプロイ job の失敗を通知する。Cloud Run は古いリビジョンを配り続けるので、通知しない限り失敗は誰にも見えない
それでも取りこぼす場面がある。
| 取りこぼす場面 | 理由 |
|---|---|
| CI のランナーと本番で CPU が違う | arm64 のランナーでビルドすると、本番とは別の prebuilt を試すことになる |
| アドオンを遅延して require している | 起動時に読まないモジュールは、起動の確認では落ちない |
| 本番にしか無い環境変数やシークレットが要る | CI でのイメージの起動は、本番の起動経路を最後まで通らない |
更新の PR 自体が止まっていないかの見張りはRenovateが黙って止まったのに気づく見張りで扱った。
まとめ
デプロイが「ポートで待ち受けられない」で落ち、アプリのログが出ていないなら、ポートより先にネイティブアドオンを疑う。エージェントには、直す前に本番と同じ条件での再現と、変数を1つずつ入れ替えた表を出させる。エージェントを使った開発の進め方を外から整えたい場合は、AI開発支援で相談を受け付けている。



