Node.js(サーバーサイドで動くJavaScript実行環境)でアプリを作ったあと、本番環境に出す段階でつまずく方に向けた内容です。ローカルで動いたコードをそのままサーバーに置いても、本番で安定稼働するとは限りません。
実際に起きるのは、環境変数の設定漏れ、ポート番号の衝突、プロセスが落ちたまま気づかない、といった地味なトラブルです。これらはフレームワーク選びよりも前に押さえるべき基礎で、Express(Node.jsの定番Webフレームワーク)やFastifyを使う場合でも考え方は共通しています。
本番デプロイで何が「半分残っている仕事」なのか
ローカル開発でnpm run devが動く状態は、全体の半分にすぎません。残り半分は、ランタイム環境の準備・自動チェックの実行・本番設定の投入・プロセス管理・死活監視の4点です。
この4点のうち、特に見落とされやすいのが「死活監視」です。アプリが起動していても、リクエストに正しく応答できているかは別問題だからです。
たとえばメモリリークでプロセスが徐々に重くなっても、OS上では「生きている」ように見えます。ここで必要になるのが、アプリ自身が自分の健康状態を報告する仕組み、つまりヘルスチェック用エンドポイントです。
ヘルスチェックエンドポイントの仕組みを段階的に見る
Node.jsの組み込みhttpモジュールだけで、最小構成のヘルスチェックを実装できます。外部パッケージなしで動かせる点が、学習にも本番導入の最初の一歩にも向いています。
基本構成は次の3段階です。まず起動時にPORT環境変数を検証し、不正な値なら即座に終了します。
const PORT = Number(process.env.PORT || 3000);
if (!Number.isInteger(PORT) || PORT < 1 || PORT > 65535) {
console.error('[startup] PORT must be an integer between 1 and 65535.');
process.exit(1);
}この「早期失敗(fail early)」の考え方は重要です。不正な設定のまま起動してしまうと、デプロイ後しばらくしてから原因不明のエラーに悩まされることになります。
次に/healthというパスへのGETリクエストに対して、稼働時間などを含むJSONを返すハンドラを用意します。多くのホスティングサービスは、このパスを定期的に叩いて応答があるかを確認し、応答がなければプロセスを再起動する仕組みを持っています。
そして最後にSIGTERMなどの終了シグナルを受け取ったとき、既存のリクエスト処理を終えてからプロセスを終了する「グレースフルシャットダウン」を実装します。これを省くと、デプロイのたびに処理中だったリクエストが強制切断されるリスクがあります。
PM2・systemd・コンテナ、プロセス管理の選択肢を比較する
アプリ本体ができたら、次はそれを「誰がどう動かし続けるか」という問題です。選択肢は大きく3つに分かれます。
- マネージドプラットフォーム: GitリポジトリをつなぐとビルドとデプロイがGUIで完結する。インフラ管理の手間が少ない反面、OSやリバースプロキシへの細かい制御は難しい
- VPS(仮想専用サーバー)+ プロセススーパーバイザー: systemdやPM2(Node.js向けのプロセス管理ツール)でプロセスを監視し、クラッシュ時に自動再起動する
- コンテナ + オーケストレータ: Dockerでイメージ化し、Kubernetesなどで複数台に展開する。再起動やスケールの仕組みを自前で持つ
どの方式でも共通する注意点があります。プロセススーパーバイザーはプロセスの再起動は面倒を見てくれますが、アプリケーション監視・バックアップ・セキュリティパッチ適用・データベースの健全性までは代行しません。
この点は、PM2を導入すれば監視が完結すると誤解しやすい部分です。PM2はあくまで「落ちたら上げ直す」仕組みであり、なぜ落ちたかを調べるログ基盤やアラート通知は別途用意する必要があります。
日本の開発現場でよく見る構成だと、小規模サービスはVercelやRenderのようなマネージド型、社内システムや既存VPS資産があるチームはsystemd+PM2、マイクロサービス構成を取るチームはコンテナ、という棲み分けが一般的です。自社のインフラ担当者の有無や、スケールの見込みに応じて選ぶ判断材料になります。
依存関係の再現性とnpm ciの役割
本番環境でよくある事故の一つが、「ローカルでは動くのに本番で依存関係が壊れる」現象です。これを防ぐのがロックファイル(package-lock.jsonなど)とnpm ci --omit=devコマンドです。
npm installはpackage.jsonの範囲内で最新のパッチバージョンを取得しようとしますが、npm ciはロックファイルに記録された正確なバージョンのみを再現します。本番ビルドではnpm ciを使うことで、開発環境と本番環境のズレを防げます。
--omit=devオプションを付けると、devDependencies(テストツールなど開発時専用のパッケージ)をインストールせずに済みます。本番イメージを軽量化し、不要な攻撃対象領域を減らす効果もあります。
今日確認できるポイント
実際に自分のプロジェクトを見直す際は、次の項目を順番にチェックしてみてください。
- package.jsonのenginesフィールドでNode.jsのLTS(長期サポート)バージョンが指定されているか
- PORTやDATABASE_URLなどの設定値がコードに直書きされず、環境変数から読まれているか
/health相当のエンドポイントが存在し、デプロイ先のヘルスチェック設定がそれを参照しているか- SIGTERM受信時に接続中のリクエストを待ってから終了する処理があるか
- 本番ビルドで
npm ci --omit=devを使っているか、CI設定ファイルを確認する
これらはNext.jsやNestJSのような上位フレームワークを使っていても、内部的にはNode.jsプロセスとして同じ原則が当てはまります。フレームワークのデプロイガイドを読む前に、素のNode.jsレベルでこの基礎が押さえられているかを確認しておくと、トラブル時の切り分けが格段に楽になります。
まとめ
Node.jsの本番デプロイは、コードをサーバーに置く作業そのものより、周辺の「見えない仕組み」の積み重ねです。
PORT検証による早期失敗、/healthエンドポイントでの死活監視、グレースフルシャットダウン、ロックファイルによる依存関係の固定という4点は、どのホスティング形態を選んでも共通の土台になります。
まずは手元のアプリにヘルスチェック用のルートがあるかを確認し、なければ数行のコードで追加するところから始めてみてください。デプロイ先の設定画面でヘルスチェックパスが正しく指定されているかも、あわせて見ておくと安心です。