
VPS で Next.js を動かしていると、こんな症状が出ることがあります。
- ページを開くと503エラーになる
- 最初の数ページは開けるのに、続けて開くと落ちる
- しばらく使っていると重くなってくる
- デプロイするたびにサーバーが不安定になる
原因はどれも「メモリの使い方」に関係しています。 この記事では、2GB の VPS で Next.js を安定稼働させるために実際に適用した3つの設定を、なぜそうするのかの理由も含めて解説します。
なぜメモリ不足が起きるのか
まず「なぜメモリ不足が起きやすいのか」を整理します。
Next.js をサーバーモード( next start )で動かすとき、サーバーは複数の仕事を同時にこなしています。
- Next.js サーバー本体:ページのリクエストに応答するためのメモリ。通常は 70〜100MB 程度。
- 画像の動的変換:アクセスがあった画像をリアルタイムでリサイズ・最適化する処理。複数の画像を同時に処理するとメモリが急増します。
- API の処理:ランキングや閲覧数など、アクセスごとにデータベースに問い合わせる処理があると、その分だけメモリを使います。
2GB というメモリは VPS の中でも手頃な価格帯のスペックですが、OS や Nginx など他のプロセスが使う分を引くと、Next.js に使える余裕は 1GB 程度になります。 通常、1GB あれば個人の小規模なブログサイトを動かすことに特段の問題はありませんが、十分な余裕があるというものでもありません。 メモリを効率的に使うための対策はやっておくことをおすすめします。
設定①:画像の動的変換を無効にする
何が問題だったのか
Next.js には、ブラウザからリクエストがあったとき画像をリアルタイムでリサイズ・変換する機能があります。
/_next/image というパスへのアクセスがあると、サーバー上でその処理が走ります。
ブログに画像が少ないページなら問題ありません。 しかし画像が多いページを複数タブで同時に開いたとき、変換処理が重なってメモリが一気に増加します。 2GB の VPS ではこのタイミングで余裕がなくなり、503エラーになることがあります。
設定の変更方法
プロジェクトルートにある next.config.ts を開きます。
bashnano ~/example-blog/next.config.ts
images の設定を以下のように変更します。
変更前:
typescriptimages: { remotePatterns: [], },
変更後:
typescriptimages: { unoptimized: true, },
unoptimized: true を指定すると、 /_next/image による動的変換処理が完全に無効になります。
画像は元のファイルをそのまま配信するようになるため、変換のためのメモリ消費がなくなります。
「画像が劣化するのでは?」という疑問への回答
unoptimized: true にすると、サーバー側での自動リサイズ・圧縮が行われなくなります。
ただし、これは「今まで自動でやってくれていたことを自分で準備する必要がある」という意味です。 あらかじめ適切なサイズに書き出した画像ファイルを使っていれば、表示品質は変わりません。
私のブログでは Obsidian で記事を書いており、添付画像はデプロイ前にリサイズ処理を挟んでいます。
そのため unoptimized: true にしても表示上の変化はありませんでした。
設定変更後は忘れずにビルドします。
bashcd ~/example-blog && ./deploy.sh
設定②:Nginx のキープアライブ接続を正しく設定する
何が問題だったのか
Nginx はリバースプロキシとして動き、ブラウザからのリクエストを Next.js に転送します。 この転送の際、「キープアライブ接続」という仕組みを使って接続を使い回すことで効率化しています。
キープアライブ接続とは、1回のリクエストのたびに接続を張り直すのではなく、同じ接続を使い回してやり取りをする仕組みです。 これにより、サーバーへの負荷と接続のオーバーヘッドを減らすことができます。
この仕組みが正しく機能しないと、リクエストが積み重なったときに処理できなくなります。 その結果が503エラーです。
原因になった設定はこれです。
nginxproxy_set_header Connection 'upgrade';
Connection: upgrade は、本来 WebSocket 接続を確立するときだけ使うヘッダーです。
WebSocket とは、チャットや通知機能などでサーバーとブラウザが常時接続するための仕組みです。
ところがこの書き方だと、通常の HTTP リクエスト(ページの読み込みなど)にもこのヘッダーを常に送り続けてしまいます。
Nginx のキープアライブ接続は「接続を使い回す」ことで成立します。
しかし upgrade ヘッダーは「プロトコルを切り替える(接続を変える)」ことを意味します。
この2つが矛盾するため、接続プールが正しく管理できなくなり、数件のリクエストの後に壊れて503エラーが出る、という症状になります。
どうやって原因を突き止めたか
Next.js に直接アクセスした場合と、Nginx 経由でアクセスした場合を比較するテストをしました。
Nginx を経由せずポート3000に直接アクセスした結果:
bashfor i in {1..25}; do curl -s -o /dev/null -w "%{http_code} " http://localhost:3000/; done
結果:200 200 200 200 200... すべて正常。
Nginx 経由でアクセスした結果:
bashfor i in {1..25}; do curl -s -o /dev/null -w "%{http_code} " https://next.example.com/; done
結果:200 200 200 200 200 200 503 503 503...
Next.js は正常なのに Nginx 経由だと6件目から503になる。これで問題が Nginx 側にあることを特定できました。
自分のサイトで503エラーが発生したときに、なかなか原因が特定できなかったのですが、このテストを実施したところ、Nginx の設定に問題があることが分かり、解決できました。
設定の変更方法
map ディレクティブを使って、通常の HTTP のときと WebSocket のときで動作を切り替えます。
Nginx の設定ファイルを開きます。
bashsudo nano /etc/nginx/sites-enabled/next.example.com.conf
設定ファイルの冒頭( upstream ブロックの前)に以下を追加します。
nginxmap $http_upgrade $connection_upgrade { default upgrade; '' ""; }
続いて location / ブロック内の記述を変更します。
変更前:
nginxproxy_set_header Connection 'upgrade';
変更後:
nginxproxy_set_header Connection $connection_upgrade;
map は Nginx の条件分岐の仕組みです。
「クライアントから Upgrade ヘッダーが来ているかどうかによって、Next.js に送る Connection ヘッダーの値を変える」という意味になります。
| クライアントからのリクエスト | Next.js に送る値 |
|---|---|
| WebSocket リクエスト( Upgrade ヘッダーあり) | upgrade |
| 通常の HTTP リクエスト( Upgrade ヘッダーなし) | 空(ヘッダーを送らない) |
通常の HTTP リクエストのときは Connection ヘッダー自体を送らなくなるため、Nginx のキープアライブ接続が正しく機能するようになります。
設定変更後は必ずテストして反映します。
bashsudo nginx -t && sudo systemctl reload nginx
nginx: configuration file ... test is successful と表示されれば成功です。
設定③:PM2 のメモリ上限を設定する
PM2 とは何か
PM2 は Node.js のプロセス管理ツールです。 Next.js サーバーをバックグラウンドで動かし続けるために使います。
pm2 start で起動したプロセスは、ターミナルを閉じても動き続けます。
またサーバーがクラッシュしたときに自動再起動する機能も持っています。
PM2 には「メモリが設定した上限を超えたら自動で再起動する」機能があります。
これを --max-memory-restart オプションで指定します。
PM2 が監視するのは「RSS」というメモリ
ここで少し専門的な話をします。
--max-memory-restart が監視しているのは RSS(Resident Set Size) と呼ばれるメモリ量です。
RSS とは、プロセスが実際に使っているメモリ全体のことで、pm2 status で表示される memory の列がこれにあたります。
pm2 show プロセス名 を実行したときに表示される「Heap Usage」や「Heap Size」とは別の数値です。
ヒープとは JavaScript のオブジェクトを保持する領域のことで、RSS の内側に含まれる一部分です。
RSS(プロセス全体のメモリ)
├── ヒープ(JavaScript オブジェクトの保持領域)
├── スタック(関数呼び出しの管理領域)
├── コード本体
└── 共有ライブラリ
--max-memory-restart は RSS 全体を見て判断するため、「ヒープ使用率が高く見えても RSS が低ければ問題ない」ということが起こります。
なぜ 400MB にするのか
上限値は「通常の動作では絶対に超えない値」かつ「VPS のメモリが限界になる前に超える値」の間に設定します。
まず、2GB VPS 上で Next.js 以外が使うメモリを概算します。
| プロセス | 消費メモリの目安 | |---|---| | Ubuntu OS 本体 | 200〜300MB | | Nginx | 50〜100MB | | その他( fail2ban 等) | 50MB 程度 | | 合計(Next.js 以外) | 300〜450MB 程度 |
Next.js に使える余裕:2048MB − 450MB = 約 1,600MB
Next.js の通常の RSS:pm2 status で確認すると 約 72〜76MB
通常時の RSS の約5倍(72MB × 5 = 360MB)を目安に、切り上げて 400MB を上限としました。
図で表すと次のようになります。
0MB 76MB 400MB 1600MB 2048MB
|------|----------|------------------------|-------|
通常稼働 ↑ここで再起動 ←余裕→ VPS上限
閾値(400MB)
通常の動作(76MB)の5倍以上に RSS が膨らんでいれば「何かがおかしい」と判断できます。 かつ VPS のメモリが限界になるはるか手前で再起動がかかるため、サーバー全体が落ちる前に自動回復できます。
ご自身のサーバーで、
pm2 statusを使って、Next.js が使用しているメモリを確認し、余裕をもった上限値を決めてください。
なぜこの設定がセーフティネットとして有効か
設定①と②でメモリの即時的な問題は解消されます。 しかし長期間運用していると、メモリリークと呼ばれる現象が起きることがあります。
メモリリークとは、プログラムが使い終わったメモリを正しく解放できず、RSS が少しずつ増え続ける現象です。 1時間で数 MB 増える程度でも、1週間・1ヶ月と運用し続けると無視できない量になります。
--max-memory-restart を設定しておくと、RSS が閾値を超えたタイミングで PM2 が自動的に再起動します。
再起動はほんの数秒で完了するため、閲覧中のユーザーへの影響は最小限です。
「問題が起きたときに自動回復するための保険」として設定しておく、というつもりで設定しておくことをおすすめします。
設定の手順
まず PM2 の現在の状態を確認します。
bashpm2 status
status が online になっていることを確認します。
次に、動いているプロセスを停止・削除します。
--max-memory-restart は起動時にしか指定できないため、いったん止めて再起動する必要があります。
bashpm2 stop example-blog pm2 delete example-blog
プロジェクトのディレクトリに移動してから起動します。
ディレクトリを移動することで、PM2 が package.json の場所を正しく認識できます。
bashcd ~/example-blog pm2 start npm --name "example-blog" --max-memory-restart 400M -- start
各オプションの意味は次の通りです。
| オプション | 意味 |
|---|---|
| pm2 start npm | npm コマンドを PM2 の管理下で起動する |
| --name "example-blog" | このプロセスに名前をつける |
| --max-memory-restart 400M | RSS が 400MB を超えたら自動再起動する |
| -- start | npm start(= next start)を実行する |
起動後、状態を確認します。
bashpm2 status
status が online、restarts が 0 になっていれば成功です。
設定を保存する(pm2 save)
次のコマンドで現在の状態を保存します。
bashpm2 save
pm2 save は「今 PM2 で動いているプロセスの設定をファイルに書き出す」コマンドです。
保存内容には --max-memory-restart 400M も含まれます。
この保存が必要な理由は、VPS の自動起動の仕組みと関係しています。
以前の作業で pm2 startup というコマンドを実行しました。
これは「VPS が再起動したときに PM2 を自動的に起動する」仕組みを登録するコマンドです。
しかし「PM2 を起動する」だけでは、どのプロセスを動かすかが決まりません。
pm2 save によって保存されたファイルを読み込むことで、初めて Next.js サーバーが自動起動されます。
VPS が再起動する
↓
pm2 startup の設定により PM2 が自動起動する
↓
pm2 save で保存したファイルを読み込む
↓
next-blog プロセスが自動的に起動する(400MB の上限設定も復元される)
↓
サイトが表示される
[PM2] Successfully saved と表示されれば保存完了です。
次回以降の起動時について
--max-memory-restart は PM2 の起動コマンドにオプションとして指定するものです。
そのため、pm2 delete してから pm2 start し直すときは、毎回このオプションを含めて起動する必要があります。
bashcd ~/example-blog pm2 start npm --name "example-blog" --max-memory-restart 400M -- start pm2 save
VPS の再起動後については、pm2 save で保存された内容が自動的に復元されるため、手動での操作は不要です。
3つの設定のまとめ
| 設定 | 対象ファイル | 内容 | 効果 |
|---|---|---|---|
| unoptimized: true | next.config.ts | 画像の動的変換を無効化する | 複数画像アクセス時のメモリ急増を防ぐ |
| map ディレクティブ | Nginx 設定ファイル | Connection ヘッダーを適切に切り替える | キープアライブ接続を正常に機能させる |
| --max-memory-restart 400M | PM2 起動オプション | RSS が 400MB を超えたら自動再起動する | メモリリーク発生時の自動回復 |
設定①と②は「今すぐ503エラーを止める」ための修正です。 設定③は「長期運用で異常が起きたときに自動で回復する」ための保険です。
これらはいずれも「メモリに余裕があれば不要」なものです。 しかし 2GB という制限の中で Next.js を安定運用するには、「何がメモリを使っているか」を意識した設計が必要でした。
503エラーの調査と修正の詳しい過程については、メインブログでまとめています。





