Docker Composeのメモリ制限でOOMを防ぐ方法
Docker Composeでdeploy.resourcesとmem_limitを設定し、1つのコンテナによるVPS全体の停止を防ぎます。OOM時の終了コード137、swap、適切な容量の決め方も解説します。
Docker Compose のメモリ制限が行うこと
Docker Compose のメモリ制限は、Linux カーネルが 1 つのコンテナの cgroup(control group。プロセス群のリソースを計測するカーネル機能)に設定する上限です。サービスに deploy.resources.limits.memory を設定すると、そのコンテナは指定した値を超えてメモリを使用できません。超えようとすると、カーネルはコンテナ内のプロセスを 1 つ強制終了します。通常、コンテナは終了コード 137 で終了します。
これは、RAM 容量が固定され、ホストから借りられる余剰メモリがない VPS で特に重要です。メモリリークや不適切なクエリがあるコンテナは、8GB のサーバー上にある空きページをすべて使い切る可能性があります。その場合、カーネルは最も問題が大きいと判断したプロセスを終了します。多くの場合、原因となったコンテナではなく、データベースや SSH セッションが終了されます。制限を設定すると、サーバー全体の停止を、再起動する 1 つのサービスの障害に抑えられます。
services:
app:
image: ghcr.io/example/app:1.4
deploy:
resources:
limits:
cpus: "1.5"
memory: 1g
reservations:
memory: 256m設定を適用し、制限が有効であることを確認します。
docker compose up -d
docker stats --no-streamMEM USAGE / LIMIT 列には、142MiB / 1GiB のような値が表示されます。制限の列にホストの全 RAM 容量が表示される場合、設定は適用されていません。適用されるまで、このガイドの以降の手順は役に立ちません。Compose ファイルを初めて使う場合は、VPS 向け Docker Compose の基本で、この手順の前提となるファイル構成を確認できます。
deploy.resources.limits と mem_limit: どちらが適用されるか
同じ考え方を表す表記が2つあるため、混乱しやすくなっています。
mem_limit、mem_reservation、memswap_limit、cpus、cpu_shares は、古い Compose ファイル形式から引き継がれたサービス直下のキーです。deploy.resources は Swarm スキーマに由来し、現在は Compose Specification の一部になっています。これは現在、docker compose が読み込む形式です。
どちらも単一ホストで動作します。Swarm クラスターがなくても、Compose V2 の docker compose プラグインは docker compose up の実行時に deploy.resources.limits と deploy.resources.reservations を適用します。deploy ブロック内で Swarm 専用なのは、その他のキーです。mode、placement、update_config、endpoint_mode は docker stack deploy では意味を持ちますが、docker compose up では無視されます。そのため、「deploy には Swarm が必要」という一般的な説明は resources サブセクションには当てはまりません。この説明に従うと、サービスに制限がまったく設定されないことになります。
プロジェクトごとにどちらか一方の表記を選んでください。同じサービスに mem_limit: 512m と deploy.resources.limits.memory: 1g を記述すると、ひと目で内容を把握できないファイルになります。どの値が適用されたか推測するのではなく、daemon に確認させてください。
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1メモリ値はバイト単位です。そのため、1g は 1073741824 と表示されます。CPU は nano CPU 単位です。そのため、1.5 は 1500000000 と表示されます。いずれかのフィールドに 0 と表示された場合、その項目には制限が設定されていません。Docker が受け付ける最小のメモリ制限は 6m です。これを下回ると、コンテナは起動しません。
コンテナが上限に達した場合
コンテナの動作が遅くなることはありません。停止します。
プロセスがページを要求した時点で、cgroup がすでに memory.max に達している場合、カーネルはまず、その cgroup 内で回収できるものを回収します。最初にクリーンなページキャッシュを回収し、次に swap へ移動できるページを回収します。それでも十分な領域を確保できない場合、cgroup の OOM(out of memory)killer がコンテナ内のプロセスを選び、SIGKILL を送信します。コンテナの PID 1 を kill すると、コンテナは終了します。終了コード 137 は単に 128 にシグナル 9 を加えた値です。そのため、137 は SIGKILL が発生したことを示すものであり、それだけで OOM の証拠になるわけではありません。
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 は OOM kill です。false 137 は別の何かが SIGKILL を送信したことを意味します。通常の原因は、アプリが SIGTERM を無視したために docker compose stop が 10 秒の猶予期間に達したことです。この違いを把握すると、両者はまったく別の問題であるため、原因調査にかかる時間を大幅に短縮できます。
このイベントは、さらに 2 か所に記録されます。まず、daemon のログをリアルタイムで監視します。
docker events --filter event=oom次に、再起動後も残る記録であるカーネルログを読みます。
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'cgroup による kill が発生すると、Memory cgroup out of memory: Killed process 24713 (node) で始まる行が出力されます。Memory cgroup プレフィックスのない行はホストの OOM です。これはマシン自体の RAM が不足したことを意味します。これを防ぐためにリソース上限を設定するため、該当する行がある場合は、設定した上限の合計が高すぎるか、一部のサービスに上限が設定されていない可能性があります。
restart: unless-stopped を使用すると、OOM ループは見つけにくくなります。サービスは停止した 1 秒後には docker compose ps で起動中に見えるためです。uptime 列と再起動回数を確認し、上限の設定に アプリを unhealthy と報告する healthcheck を組み合わせてください。これにより、監視していなくても停止を繰り返すコンテナを確認できます。
予約は目安であり、上限がルールです
reservations.memory(旧称 mem_reservation)は、メモリ使用量の下限を緩やかに指定する設定です。Docker はこれを、daemon がホスト上のメモリ競合やメモリ不足を検知したときに有効になるソフトリミットとして説明しています。コンテナがこの値を超えることを防ぐものではなく、コンテナがメモリを要求した時点で、そのメモリが空いていることを保証するものでもありません。コンテナが reservation を超えている場合に、そこから先にメモリを回収するよう kernel に優先度を与えるだけです。
したがって、reservation だけでは何も保護できません。負荷が高い状況でも優先的に扱いたいサービスを示すために使用し、安全性は limit に任せてください。reservation は limit より小さくしてください。そうしないとコンテナは起動しません。Docker は Minimum memory limit can not be less than memory reservation limit として設定を拒否します。
スワップの使用量を正しく把握する
ほとんどの VPS イメージには、スワップファイルがまったくありません。swapon --show と free -h を実行します。スワップの合計が 0 の場合、以下のスワップ関連設定はすべて機能せず、メモリ制限は RAM の上限だけになります。
memswap_limit はスワップ容量ではありません。メモリとスワップの合計です。mem_limit: 1g と memswap_limit: 2g を指定すると、コンテナには 1GB の RAM と 1GB のスワップが割り当てられます。2 つの値を同じにすると、コンテナはスワップをまったく使用できません。mem_limit を設定し、memswap_limit を未設定にすると、コンテナは再びメモリ制限のサイズまでスワップを使用できます。
Ubuntu 24.04 と Debian 13 はデフォルトで cgroup v2 を使用します。この場合、スワップは別のカウンター(memory.swap.max)として扱われ、追加設定なしで機能します。古いメッセージ Your kernel does not support swap limit capabilities は、swapaccount=1 なしで起動した cgroup v1 ホストで表示されます。その環境でもメモリ制限は適用されますが、スワップ部分は無視されます。
スワップによる効果を正しく理解してください。スワップは OOM kill を発生しにくくするのではなく、遅くします。メモリリークがあるプロセスは、RAM と同じようにスワップも使い切るためです。一方、共有 VPS ストレージ上でコンテナがスワップを過剰に使用すると、同じホスト上の他のすべてのサービスも遅くなります。レイテンシーが重要な処理では、スワップなしで適切な制限を設定したほうが、より速く、予測可能な形で失敗します。
実際よりメモリ使用量が多く見える理由
docker stats の MEM USAGE の値にはページキャッシュが含まれるため、大きなファイルを読み取るコンテナは上限近くまで使用量が増え、そのまま維持されます。これは正常な動作であり、メモリリークではありません。OOM killer が呼び出される前に、クリーンなキャッシュが回収されるためです。自ホスト型の Jellyfin メディアサーバー などのサービスが上限近くで恒常的に動作しているように見えるのは、まさにこのためです。
コンテナ内から、値をキャッシュと実際のワーキングセットに分けて確認します。
docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.eventsanon は匿名メモリです。解放できないワーキングセットに相当します。file はページキャッシュで、解放できます。上限は合計値ではなく、anon に余裕を加えた値を基準に設定します。memory.events ファイルを確認すれば、状況を明確に判断できます。oom_kill カウンターが 0 より大きい場合、起動後にこのコンテナ内でカーネルが何らかのプロセスを強制終了しています。max カウンターが増加している場合、コンテナは現在上限に達した状態です。どちらのコマンドにも、イメージ内のシェルと coreutils が必要です。そのため、distroless イメージや scratch イメージでは実行できません。
8GB VPS のメモリ上限
アプリケーションではなく、ホストから確認します。8GB VPS では、kernel、Docker daemon、sshd、journald、自分の login shell のために約1GBを残します。利用できるのはおよそ7GBなので、すべてのコンテナに設定する上限の合計をこの範囲内に収めます。過剰割り当ては、2つのサービスが同時にピークへ達する日までは機能します。
8GB のホストでの分割例:
- Reverse proxy:
128mlimit。小さなプロセスなので、この程度の厳しい上限を設定すると、異常な設定再読み込みをすぐに検出できます。 - PostgreSQL:
2glimit。データベース設定でshared_buffersを約512MBに設定します。 - Application container:
1glimit。 - Background worker:
512mlimit。 - Media or file service:
2glimit。その大部分は page cache に使用されます。
この数値を自分の構成にそのまま適用しないでください。実際の負荷をかけた状態でサービスを1日稼働させ、docker stats を監視します。コンテナごとに anon のピーク値を記録し、その約半分を余裕として加えます。上限が厳しすぎると、通常のネットワークトラフィックの急増で正常なサービスまで停止するため、上限を設定しない場合より悪い結果になります。
注意すべき落とし穴が1つあります。上限をランタイムに伝えない限り、多くのランタイムはその値を認識しません。PostgreSQL は shared_buffers と work_mem をコンテナの上限を超えて設定できてしまい、プロセスが kill されます。JVM (Java virtual machine) では、ホストの RAM ではなく cgroup の上限を基準にヒープサイズを設定するために -XX:MaxRAMPercentage=75 が必要です。Node.js では、コンテナの上限より低い値をメガバイト単位で --max-old-space-size に設定する必要があります。設定しないと、garbage collector によってヒープが拡大し続け、最終的に kernel が介入します。Ollama も設定項目は異なりますが同じです。num_ctx を増やすと KV cache が数百MB増加するため、長いプロンプトの途中でコンテナが停止します。cgroup は調整しません。プロセスを kill します。
CPU 制限はまったく異なる動作をします
cpus: "1.5" は、CFS(completely fair scheduler)のクォータとして適用される、1 コアの 150% に相当します。コンテナは 100ms の各期間で 150ms 分の CPU 時間を取得し、その時間をすべてのスレッドで共有します。使い切ると、カーネルは次の期間まで待機させます。
ここが重要な違いです。メモリ制限を超えたコンテナは強制終了されます。CPU 制限を超えたコンテナはスロットリングされ、処理速度が低下した状態で動作を続けます。そのため、CPU 制限は厳しめに設定しても安全ですが、メモリ制限には余裕が必要です。
cpu_shares は別の仕組みです。これは、CPU が実際に飽和している場合にのみ効果を持つ相対的な重みです。shares が 1024 のコンテナと 512 のコンテナが負荷の高い 1 コアを共有すると、概ね 2 対 1 に分配されます。一方、ホストがアイドル状態なら、どちらも制限されません。shares はサービスの重要度の順位付けに使用し、実際の上限が必要な場合は cpus を使用します。たとえば、夜間のトランスコード処理によって Web サーバーがリソース不足にならないようにできます。
FAQ
deploy.resources.limits は Docker Swarm なしでも機能しますか
はい。単一ホストで docker compose up を実行すると、Compose V2 は deploy.resources.limits と deploy.resources.reservations を適用します。docker inspect --format '{{.HostConfig.Memory}}' <container> で確認できます。このコマンドは制限値をバイト単位で表示し、制限が適用されていない場合は 0 を表示します。Swarm が実際に必要となる deploy 内のキーは、mode、placement、update_config、endpoint_mode です。
Docker Compose の終了コード 137 は何を意味しますか
メインプロセスが SIGKILL を受け取ったことを意味します。137 は 128 にシグナル 9 を加えた値です。一般的な原因はカーネルの OOM killer ですが、アプリが SIGTERM を無視した場合は、シャットダウンのタイムアウトでも同じコードになります。docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> を実行すると、両者を区別できます。true 137 はメモリによる kill で、false 137 はそうではありません。
mem_limit と deploy.resources.limits.memory のどちらを設定すべきですか
docker compose ではどちらも機能します。deploy.resources.limits.memory は現在の Compose Specification 形式であり、新しいファイルではこちらを既定にするのが適切です。ファイルの他の部分ですでに古いトップレベルキーを使用している場合は、mem_limit を維持してください。同じサービスに両方を設定するとファイルが読みにくくなるだけなので、どちらか一方を選び、docker inspect で結果を確認してください。
コンテナが kill されず、メモリ上限いっぱいの状態になるのはなぜですか
docker stats の使用量にはページキャッシュが含まれます。カーネルはメモリ圧迫時にページキャッシュを破棄するため、OOM kill は発生しません。docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat を実行し、再利用できないワーキングセットである anon の値を確認してください。file の値が高く、anon の値が低い場合、そのコンテナはディスク入出力を行っており、停止寸前という意味ではありません。
8GB の VPS では、どの程度の RAM を未割り当てで残すべきですか
カーネル、Docker daemon、sshd、journald、自分の shell 用に約 1GB を残し、すべてのコンテナの制限値の合計を残りの 7GB 未満にしてください。実際の負荷をかけた状態で 1 日運用し、各コンテナのピーク時の anon 値を確認してから数値を決めてください。合計値は埋める目標ではなく、予算として扱います。