2GBのVPSで next build を回して落ちないか、私はずっと不安でした。プランの数字を並べたページはいくらでも出てきますが、自分の構成で本当にどうなるのかは、これまで測っていませんでした。

測りました。落ちませんでした。ただ、記録を見ると「余裕でした」とも書きにくい結果です。

4GBとは比べていません。 比べられなかった理由は最後に書きます。

先に、結果だけ

63日連続で動いている本番のVPS(ConoHa の2GBプラン・3コア)で、ビルドキャッシュを使わずに Next.js のビルドを1回実行し、1秒ごとにメモリを記録しました。

ビルド            docker compose build --no-cache frontend
終了コード         0(成功)
所要時間           3分26秒
available         1,048 → 470 MiB   ビルド中の最小値。578MiB 減った
スワップ           313 → 453 MiB     140MiB 増えた(計測中の最大は 500 MiB)
ビルドキャッシュ    28.9 → 31.21 GB   この1回で 2.31GB 増えた
サイト             ビルド後に外から1度 HTTP 200 を確認(ビルド中は見ていない)

通りました。そして、通っている途中でスワップが140MiB増えています。

この構成・この1回では成功しました。同時に、スワップ使用量も増えています。限界まで負荷を増やした試験でも、スワップ無しとの比較でもありません。

何を、どう測ったか

このサーバーは記事02で46日目に測ったものと同じ箱です。Docker のコンテナが6つ動いています(Next.js / Django / PostgreSQL / nginx / certbot / redis)。

ビルドしたのはこのうち Next.js のフロントエンドだけで、中身はこうなっています。

要約すると、この3手です(実際のファイルには COPY などの行も入っています)。

FROM node:22-alpine  →  npm ci  →  npm run build

計測用のスクリプトを1つ動かし、その中でビルドを実行して、1秒ごとに free の値を記録しました。サンプルは205点です。

なぜキャッシュを使わなかったか

そのまま docker compose build を打つと、おそらく数秒で終わります。ビルドキャッシュが357エントリ・28.9GB 残っていて、8月13日に測ったときから357エントリ・28.9GB・回収可能26.02GB のどれも動いていないからです。ソースが変わっていなければ全部キャッシュに当たり、メモリはほとんど使われません。それでは測ったことになりません。

--no-cache を付けると npm ci からやり直します。これから借りる人が最初にやるデプロイと、同じ条件です。

本番のイメージを差し替えないようにした

もう1つ、注意した点があります。私の compose ファイルには image: の指定が無いので、ビルドすると、できたイメージに本番と同じ名前が付きます。動いているコンテナはそのままですが、あとで誰かが up -d を打った瞬間に、計測用のビルドがデプロイされてしまいます。

避けるには、プロジェクト名を捨て用のものに変えます。

docker compose -p measure-tmp -f docker-compose.yml -f docker-compose.prod.yml build --no-cache frontend

こうするとイメージ名が変わるので、本番のほうには触りません。この構成はビルドの引数に環境変数を参照していないため、プロジェクト名を変えてもビルドの中身と重さは同じです。終わったら消しました。

1回だけです

この計測は1回きりです。 3回まわして中央値を取る、といったことはしていません。理由は2つあって、片方は後の節に書きます。もう片方は単純に、本番のサーバーだからです。

ビルド前の状態

出発点の数字を置いておきます。2026年8月30日、ビルドを始める直前です。

  • コンテナ6つの合計メモリ … 306.4 MiB(backend 147.4 / frontend 126.7 / certbot 12.13 / nginx 10.5 / postgres 7.848 / redis 1.824)
  • メモリ … total 1,966 / used 917 / free 289 / buff-cache 1,021 / available 1,048(単位はMiB)
  • スワップ … total 2,047 のうち 313 使用、1,734 空き(単位はMiB)
  • uptime / load average … 63日 / 0.44, 0.24, 0.20
  • ディスク … 99GB中 37GB 使用(40%)

記事02 で46日目に測ったときと、ほとんど同じです。コンテナの合計は 333MiB から 306.4MiB へ少し下がっていました。

ビルドキャッシュの集計値は357エントリ・28.9GBのままでした。ただし、前後の集計値が同じことだけで、この17日間の全ビルド履歴までは確定できません。平常時の余裕とビルド時の余裕は分けて見る必要があります。

ビルド中に動いた数字

見たものビルド前ビルド中ビルド後
available1,048 MiB470 MiB(最小)—
スワップ使用量313 MiB500 MiB(最大)453 MiB
ビルドキャッシュ28.9 GB / 357件—31.21 GB / 376件

available は578MiB 減って、470MiB で底を打ちました。2GBの箱に対して、残りは4分の1弱です。

スワップは140MiB 増えました。計測中に一度500MiB まで上がり、終わった時点で453MiB です。

「まだ470MiB 余っているのに、なぜスワップが増えるのか」と思いました。この問いの立て方のほうが間違っていました。

available は「空いているメモリ」ではありません。カーネルの公式ドキュメントは、MemAvailable をこう定義しています。

An estimate of how much memory is available for starting new applications, without swapping. Calculated from MemFree, SReclaimable, the size of the file LRU lists, and the low watermarks in each zone.

(Linux kernel documentation — proc.rst)

空きメモリそのものではなく、ファイルキャッシュと各ゾーンの下限値から計算した見積もりです。

そして回収は、メモリが尽きてから始まるわけでもありません。

As the load increases, the amount of the free pages goes down and when it reaches a certain threshold (low watermark), an allocation request will awaken the kswapd daemon.

(Linux kernel documentation — concepts.rst)

同じ節は、回収の対象を "page cache and anonymous memory" としています。下限に触れた時点で回収が始まり、匿名メモリもその対象になる。

⚠️ ここで下限がかかっているのは available ではなく free です。 ビルド前の free は289MiB でした。available の1,048MiB とは別の数字です。空きが尽きる前にスワップが増えること自体は、仕組みとして起こりえます。

⚠️ ただし、今回それが起きたとは書けません。 このサーバーの vm.swappiness は測っていませんし、1秒ごとの free の値だけでは、どの順序で何が回収されたかまでは読み取れません。

言えるのは、ビルドを始める前より140MiB 多くのデータがディスク側に置かれた状態で、ビルドが終わったということだけです。

サイトについては、ビルドが終わったあとに外から1度 curl して、200 が返ることを確認しました。 ビルド中は見ていません。ビルドはイメージを作る作業なので動いているコンテナを止めませんが、止まらなかったことを連続で確認したわけではありません。

記事02 に残していた宿題

VPSは2GBで足りるかには、こう書いていました。

294MiB がスワップに逃がされていました。

常時動いているのが合計333MiBであることを考えると、これを押し出したのは普段の動作ではなさそうです。一時的に大きくメモリを使った処理があったのだろう、と考えました。

心当たりはビルドでした。そこまで書いて、その先は測っていませんでした。

今回確認したのは、この1回のビルドの前後でスワップ使用量が140MiB増えたことです。どのプロセスのページが移されたかや、工程ごとの寄与までは切り分けていません。

⚠️ ただし、294MiB の全部がビルドによるものだったとは言えません。 この記事の数字がそれを示しています。

2026-08-13   スワップ 294 MiB
2026-08-30   スワップ 313 MiB(ビルド前)

次の計測を始める前にも19MiB増えていました。ビルドキャッシュの集計値は同じでしたが、全ビルド履歴やスワップの原因内訳を記録したわけではありません。元の294MiBも、この19MiBも原因は切り分けていません。

そしてもう1つ。当時は同じ流れで、限界もビルド側に出るという書き方をしていました。今回分かったのは、ビルド中にavailableが減り、スワップが増えたことです。 限界まで負荷をかけた試験ではありません。記事02のほうも、あわせて直しました。

ディスクのほうも、輪郭は見えました。キャッシュ無しのビルド1回で2.31GB 増えます。28.9GB は、こうした積み重ねでできたものです。⚠️ ただし毎回2.31GB ずつ増えるわけではありません(キャッシュに当たれば増えません)。何回ぶんかは数えていません。

測り方でつまずいた2つ

ピークRSS が 7MB と出た

/usr/bin/time -v を通してビルドを実行したので、ビルドが使ったメモリの最大値も取れるつもりでいました。出てきたのは 7,168 KB、つまり7MBです。Next.js のビルドが7MBで済むとは思えません。

測っていたのは docker compose を呼び出した側のプロセスでした。実際のビルドは Docker のビルドエンジンが別のプロセスで走らせるので、time から見える範囲の外にあります。

docker build を time で包んでも、ビルドプロセスのピークは取れないと考えたほうがよさそうです。取るなら、外から free を定期的に見るほうです。この記事の数字が全部 available とスワップになっているのは、そのためでした。

本番で状態を戻しての再試験はしなかった

もう1つ。ビルド終了時のスワップ使用量は453MiBで、開始時の313MiBとは違っていました。

この直後に繰り返しても、同じ初期状態からの測定にはなりません。今回の本番サーバーでは、比較のためにスワップの無効化・再有効化などで状態を戻す操作はしませんでした。

スワップの増分を再測定すること自体はできます。ただし、初期状態や負荷をそろえた試験環境が必要です。今回はその準備と再試験を行わず、availableもスワップも1回ぶんの記録として残しました。

この構成では、ビルドに上限が掛かっていなかった

計測の準備で設定を読んでいて、気づいたことがあります。ベースの docker-compose.yml の frontend には、Node のヒープ上限が書いてあります。

environment:
  NODE_OPTIONS: --max-old-space-size=1536

書いてある場所は environment: の下、つまりコンテナを動かすときの環境変数としてです。build.args には同じ指定がありません。そしてビルドを走らせる RUN npm run build にも、上限は何も付いていません。

自分のファイルを読んだかぎり、この構成のビルドには上限が効いていない、ということになります。

一般論として「environment: に書いたものはビルドに渡らない」と言い切れるかどうかは、公式ドキュメントで確かめきれませんでした。Compose の仕様には environment を「コンテナに設定される環境変数」と定義した一文がありますが、ビルド時に渡らないと明示した記述は、私が見た範囲では見つけられていません。なのでここは、自分の構成についての観察に留めます。

そして今回は、上限を掛けないまま通りました。掛けたらどうなるかは測っていません。 上限を掛ければスワップが減るのか、それとも途中でビルドが落ちるのか、どちらも試していないので書けません。気になっている、というだけです。

測っていないこと

この記事で書けないことを並べておきます。

  • 4GBならどうなるか。 ConoHa のプラン変更は、サーバーを停止しないとできません(4社の公式ドキュメントを読み比べた記事で確認しています)。本番を止められないので、比較していません
  • 他社のVPSでどうなるか。 契約しているのは ConoHa の1社だけです
  • バックエンド(Django)のビルド。 今回測ったのはフロントエンドだけです
  • ヒープ上限を掛けた場合。 上の節のとおりです
  • npm ci と next build のどちらが重かったか。 --no-cache なので計測区間には両方が入っています。1秒ごとの記録はありますが、工程ごとには切り分けていません

記事02では、同じVPSでのビルドや処理追加に余裕を持たせる候補として4GB以上を挙げています。これは推定で、必要最小容量でも成功保証でもありません。今回の実測で増えた事実は、この構成の2GBで1回通ったことです。

私の環境にあって、あなたの環境に無いかもしれないもの

同じ数字が出るかどうかは、次の条件に左右されます。この計測の前提として書いておきます。

  • スワップが2,047MiB 確保されていて、1,734MiB 空いていました。 スワップが無い、または小さいサーバーだと、140MiB の逃がし先がありません
  • ディスクの空きが58GB ありました。--no-cache のビルド1回で2.31GB 増えるので、残りが少ないと別の理由で止まります
  • ビルドしたのはフロントエンド1つです。同時に2つ以上ビルドすれば、当然もっと使います
  • 動いているコンテナ6つの合計が306.4MiB でした。ここが重いと、出発点の available が違います
  • 深夜で、アクセスがほとんど無い時間帯でした
  • Node 22 の alpine イメージ、Next.js のビルドです。他のフレームワークの数字ではありません

で、サーバーでビルドするか、外でビルドするか

ここからは、上の数字をどう読むかという話です。

2GBのサーバーの上でビルドを回すこと自体は、私の構成ではできました。ただ、余裕は470MiB まで落ちます。ここに何かを足せば、その分だけ底が浅くなります。

  • コンテナをもう1つ増やす
  • バックエンドも同時にビルドする
  • ビルド中にアクセスが来て、アプリ側がメモリを使う

どれも、やったら落ちるとまでは言えません。ただ、470MiB という残りは、こうしたものを何個か重ねるには薄いと思いました。

もう1つの道は、手元のPCやCIでサーバーアプリの成果物・Dockerイメージを作り、VPSへ配ることです。ビルド処理は外せても、VPSでアプリやDBを動かす容量は必要です。私のVPSアプリについて、外部ビルドから配布・稼働まで試した記録はありません。

これは静的エクスポートとは別の選択です。静的な画面を配り、別のAPIを呼ぶ構成もあります。どの処理をどこで動かすかの区別は記事02の後半に整理しました。公式の構成はNext.js 15 Self-HostingとStatic Exportsを参照しています(2026年9月8日確認)。

決めるときに先に見るのは、プランの数字よりも「どこでビルドするか」だと思います。そこが決まらないと、必要なメモリの見積もりが立ちません。

まとめ

  • 2GBのVPSで、キャッシュ無しの Next.js ビルドは通った。3分26秒、終了コード 0
  • ただし available は 1,048 → 470 MiB まで落ち、スワップが140MiB 増えた
  • ⚠️ サイトはビルドの後に外から1度 HTTP 200 を確認しただけで、ビルド中は見ていない
  • ディスクはこの1回で2.31GB 増えた。28.9GB のキャッシュは、こうした積み重ねでできている
  • 1回だけの計測。本番で状態を戻して条件をそろえる再試験は行っていない
  • 4GBとの比較はしていない。プラン変更にサーバーの停止が要るため
  • 外部ビルドと静的配信は別の選択。どちらを選んでも、残るサーバー処理の容量は別に考える