Docker Composeのメモリ制限でOOMを防ぐ設定
Docker Composeでdeploy.resourcesとmem_limitを使い、1つのコンテナによるVPS全体の停止を防ぎます。終了コード137、swap、適切な容量の決め方も解説します。
Docker Compose のメモリ制限が行うこと
Docker Compose のメモリ制限は、Linux カーネルが 1 つのコンテナの cgroup(control group。プロセス群のリソースを計測するカーネル機能)に設定する上限です。サービスに deploy.resources.limits.memory を設定すると、そのコンテナは指定した値を超えてメモリを使用できません。上限に達すると、カーネルはコンテナ内のプロセスを強制終了します。通常、コンテナは終了コード 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 file 形式から引き継がれたサービスレベルのキーです。deploy.resources は Swarm schema から導入され、現在は Compose Specification の一部になっています。これは、現在 docker compose が読み込む形式です。
どちらも単一ホストで動作します。Docker Compose の docker compose plugin は、Swarm cluster が存在しない場合でも、docker compose up の実行時に deploy.resources.limits と deploy.resources.reservations を適用します。plugin は現在 v5 系で、2026-10-10 の確認時点で最新の安定版は v5.6.0 でした。旧 Compose file 形式のバージョンとの混同を避けるため、v2 から v5 へ移行し、v3 と v4 は飛ばされました。v5 のリリースによって、リソース制限の扱いが変更されたことはありません。deploy ブロックのうち Swarm を前提とする部分は、placement、update_config、endpoint_mode などの他のキーです。これらは docker stack deploy のために、cluster 内のノード間でスケジューリングやロールアウトを行う方法を定義します。そのため、「deploy には Swarm が必要」という一般的な説明は、resources サブセクションについては誤りです。この説明に従うと、サービスには制限がまったく設定されません。
プロジェクトごとに、どちらか一方の表記を選んでください。同じサービスに mem_limit: 512m と deploy.resources.limits.memory: 1g を併記するのは、単に読みにくいだけではありません。Compose Specification では、mem_limit を設定する場合、limits.memory と整合していなければならないと定めています。しかし、両者の値が異なる場合にどちらが優先されるかは記載されていません。どちらの表記を使う場合でも、daemon が実際に適用した値を確認してください。
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1メモリ値は bytes で表されるため、1g は 1073741824 と表示されます。CPU は nano CPUs 単位で表されるため、1.5 は 1500000000 と表示されます。いずれかのフィールドに 0 が表示される場合、そのフィールドには制限が設定されていません。Docker が受け付ける最小のメモリ制限は 6m であり、これを下回ると container は起動しません。
コンテナが上限に達するとどうなるか
コンテナの動作が遅くなるわけではありません。停止します。
プロセスがページを要求した時点で、cgroup がすでに memory.max に達している場合、kernel はまず、その cgroup 内で解放できるものを回収します。最初にクリーンな page cache を回収し、次に swap へ移動できるページを回収します。それでも十分なメモリを解放できない場合、cgroup の OOM (out of memory) killer がコンテナ内のプロセスを選び、SIGKILL を送信します。コンテナの PID 1 を強制終了すると、コンテナも終了します。終了コード 137 は単に 128 に signal 9 を加えた値です。そのため、137 は SIGKILL が送られたことを示す識別情報であり、それだけで OOM の発生を証明するものではありません。
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 は OOM による強制終了です。false 137 は通常、別の何かが SIGKILL を送信したことを示します(プログラムが意図的に 137 で終了することもありますが、まれです)。通常の原因は、アプリが SIGTERM を無視したために docker compose stop が 10 秒の猶予期間に達したことです。この違いを把握すると、両者はまったく別の問題であるため、原因調査にかかる時間を大幅に短縮できます。
このイベントは、さらに 2 か所に記録されます。daemon の動作をリアルタイムで監視します。
docker events --filter event=oom次に kernel log を読みます。これは再起動後も残る記録です。
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'cgroup による強制終了が発生すると、Memory cgroup out of memory: Killed process 24713 (node) で始まる行が出力されます。この行は、1 つのコンテナが自身の上限に達したことを示します。host に十分な空き RAM が残っている場合でも表示されることがあります。Memory cgroup prefix のない行は host OOM です。これはマシン自体の RAM が不足したことを意味します。これを防ぐために上限を設定します。そのため、この行が表示された場合は通常、設定した上限の合計が大きすぎるか、一部のサービスに上限が設定されていないか、Docker の外部で動作する何かが確保していた余裕を超えてメモリを使用しています。
restart: unless-stopped を使用すると、OOM ループが見えにくくなります。サービスは停止した 1 秒後には docker compose ps で起動しているように表示されるためです。uptime 列と再起動回数を確認し、上限と アプリが unhealthy であることを報告する healthcheck を組み合わせてください。そうすれば、監視していなくても停止を繰り返すコンテナを把握できます。
予約は目安であり、制限がルールです
reservations.memory(旧称 mem_reservation)はソフトな下限です。Docker では、daemon がホスト上のメモリ競合またはメモリ不足を検出したときに有効になるソフトリミットと説明されています。これによってコンテナがその値を超えることは妨げられません。また、コンテナが要求した時点でメモリが空いていることも保証されません。予約値を超えているコンテナから先にメモリを回収するよう、kernel に優先順位を付けるだけです。
したがって、reservation だけでは何も保護できません。負荷が高い状況でも優先的に扱いたいサービスを示すために使用し、安全性は limit に依存してください。reservation は limit より小さく設定してください。そうしないとコンテナは起動せず、Docker は Minimum memory limit can not be less than memory reservation limit を返して設定を拒否します。
Swap accounting、正直なところ
ほとんどの VPS イメージには、swap ファイルがまったくありません。swapon --show と free -h を実行します。swap の合計が 0 の場合、以下の swap 関連設定はすべて機能せず、メモリ制限は純粋な RAM の上限になります。
memswap_limit は swap の容量ではありません。メモリと swap の合計です。mem_limit: 1g と memswap_limit: 2g を指定すると、コンテナには 1GB の RAM と 1GB の swap が割り当てられます。2 つの値を同じにすると、コンテナでは swap がまったく使われません。mem_limit を設定し、memswap_limit を未設定のままにすると、コンテナは再びメモリ制限のサイズまで swap を使用できます。
Ubuntu 24.04 と Debian 13 はデフォルトで cgroup v2 を使用します。ここでは swap が独立したカウンター(memory.swap.max)として扱われるため、追加設定なしで機能します。古いメッセージ Your kernel does not support swap limit capabilities は、swapaccount=1 なしで起動した cgroup v1 ホストで発生します。この場合もメモリ制限は適用されますが、swap の部分は無視されます。
swap に何が得られるのかを正しく理解してください。swap は OOM kill の発生を遅らせますが、発生しにくくするものではありません。メモリリークするプロセスは、RAM と同じように swap も使い切るためです。一方、共有 VPS ストレージ上で swap を多用するコンテナが動作すると、同じホスト上の他のサービスも遅くなります。レイテンシーが重要な用途では、swap なしで適切な制限を設定したほうが、より早く、予測しやすい形で失敗します。
メモリ使用量が実際より多く見える理由
Linux では、docker stats の MEM USAGE の値は非アクティブなページキャッシュ(cgroup v2 では inactive_file)だけを差し引きます。そのため、最近使用されたキャッシュは引き続き使用量に含まれます。また、大きなファイルを読み取るコンテナは上限近くまで使用量が増え、その状態に留まります。これは正常な動作であり、リークではありません。OOM killer が呼び出される前に、クリーンなキャッシュが回収されるためです。self-hosted 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 より大きい場合、起動後にこのコンテナ内でカーネルが何かを kill したことを示します。max カウンターが増加している場合、コンテナは現在も上限に達した状態です。どちらのコマンドもイメージ内に shell と coreutils が必要です。そのため、distroless または scratch イメージでは実行できません。
8GB VPS のメモリ上限
アプリケーションではなく、ホストから考え始めます。8GB の VPS では、kernel、Docker daemon、sshd、journald、自分の login shell 用に約 1GB を残します。利用できるのはおよそ 7GB なので、すべての container limit の合計をこの範囲内に収めます。過剰な割り当ては、2 つのサービスが同時にピークに達する日までは問題なく動作します。
8GB のホストで現実的な配分は次のとおりです。
- Reverse proxy: 128m limit。小規模なプロセスなので、この程度まで制限すると、暴走した設定 reload をすぐに検出できます。
- PostgreSQL: 2g limit。データベース設定では
shared_buffersを約 512MB に設定します。 - Application container: 1g limit。
- Background worker: 512m limit。
- Media または file service: 2g limit。その大部分は page cache に使われます。
これらの数値を自分の stack にそのまま適用しないでください。サービスを実際の負荷で 1 日稼働させ、docker stats を監視します。container ごとのピーク時の anon 値を取得し、その約 1.5 倍を余裕として設定します。limit が厳しすぎると、通常の network traffic の急増時に正常なサービスを停止させるため、limit を設定しない場合より悪影響が大きくなります。
1 つ注意すべき点があります。一部の runtime は、明示的に通知しない限り limit を無視します。また、limit を読み取る runtime でも、期待どおりに自身のサイズを調整するとは限りません。PostgreSQL は shared_buffers と work_mem を container limit より大きく設定できてしまい、その結果 kill されます。最新の JVM (Java virtual machine) はデフォルトで cgroup limit を検出します。ただし、OpenJDK などの HotSpot build は、その 25 percent だけを heap に割り当てます。そのため、heap 外のメモリ用の余地を残しながら heap に limit をより多く使わせるには、通常 -XX:MaxRAMPercentage=75 を指定します。Node.js は version 12 以降、cgroup limit を基に default heap を設定します。container limit の約半分を使用し、約 2GB で頭打ちになります。異なる上限にしたい場合だけ --max-old-space-size を megabytes 単位で設定し、kernel が介入する前に garbage collector が動作できるよう、container limit より小さくします。Ollama も設定項目は異なりますが同じ問題があり、num_ctx を増やすと KV cache が数百 megabytes 増加します。その結果、長い prompt の途中で container が停止します。cgroup は交渉しません。kill します。
CPU 制限はまったく異なる動作をします
cpus: "1.5" は 1 コアの 150% に相当し、CFS(completely fair scheduler)の quota として適用されます。コンテナは 100ms の各期間で 150ms 分の CPU 時間を取得し、その時間をすべてのスレッドで共有します。使い切ると、kernel により次の期間まで待機させられます。
ここが重要な違いです。メモリ制限を超えたコンテナは kill されます。CPU 制限を超えたコンテナは throttling され、処理速度が落ちた状態で動作を続けます。そのため、CPU 制限は余裕を持たせず積極的に設定しても安全ですが、メモリ制限には余裕が必要です。
cpu_shares は別の機能です。CPU が実際に飽和している場合にだけ作用する相対的な weight です。shares が 1024 と 512 の 2 つのコンテナは、負荷の高い 1 コアをおよそ 2 対 1 で分け合います。一方、ホストがアイドル状態なら、どちらも制限されません。shares はサービスの重要度の順位付けに使用し、実際の上限が必要な場合は cpus を使用します。たとえば、夜間の transcode ジョブによって Web サーバーの処理が滞るのを防げます。
FAQ
Docker Swarm なしでも deploy.resources.limits は機能しますか?
はい。Docker Compose と docker compose plugin(現在は v5 系)は、単一ホスト上で docker compose up を実行すると deploy.resources.limits と deploy.resources.reservations を適用します。docker inspect --format '{{.HostConfig.Memory}}' <container> を実行して確認できます。このコマンドは制限値を bytes 単位で表示し、制限が適用されていない場合は 0 を表示します。Swarm を想定して deploy 内に記述するキーは、placement、update_config、endpoint_mode などのクラスタ関連のキーです。
Docker Compose の exit code 137 は何を意味しますか?
ほぼ常に、メインプロセスが SIGKILL を受信したことを意味します。137 は 128 に signal 9 を加えた値です(プログラムが意図的に 137 で終了することもありますが、まれです)。一般的な原因は kernel の OOM killer ですが、アプリが SIGTERM を無視すると shutdown timeout でも同じコードになります。docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> を実行すると、両者を区別できます。true 137 は memory kill で、false 137 は memory kill ではありません。
mem_limit と deploy.resources.limits.memory のどちらを設定すべきですか?
docker compose ではどちらも機能します。deploy.resources.limits.memory は現在の Compose Specification で使用する形式であり、新しいファイルではこちらをデフォルトにするのが適切です。ファイルの他の部分ですでに古いトップレベルキーを使用している場合は、mem_limit を維持してください。Compose Specification では、両方を設定した場合に値を一致させる必要があります。そのため、どちらか一方を選び、docker inspect で結果を確認してください。
コンテナが full memory limit まで使用しているのに kill されないのはなぜですか?
docker stats の usage 値には、最近使用された page cache も含まれます。kernel はメモリー不足の際にこの cache を破棄するため、OOM kill は発生しません。docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat を実行し、回収できない working set を示す anon 値を確認してください。file が高く、anon が低い場合、そのコンテナは disk input and output を行っています。コンテナがまもなく停止することを示しているわけではありません。
8GB VPS では、どの程度の RAM を未割り当てで残すべきですか?
kernel、Docker daemon、sshd、journald、自分の shell 用に約 1GB を残し、すべてのコンテナの limit の合計を残りの 7GB 未満に抑えてください。値を決定する前に、実際の負荷をかけて 1 日運用し、コンテナごとの peak anon 値を確認してください。合計値は使い切る目標ではなく、予算として扱います。