Docker Composeのメモリ制限でOOMを防ぐ方法
Docker ComposeでメモリとCPUの上限を設定し、1つのコンテナによるVPS全体のOOMを防ぎます。deploy.resourcesとmem_limitの違い、終了コード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 ファイル形式から継承されたサービスのトップレベルキーです。deploy.resources は Swarm スキーマに由来し、現在は Compose Specification の一部です。これは現在 docker compose が読み取る形式です。
どちらも単一ホストで動作します。Compose V2 の docker compose プラグインは、Swarm クラスターが存在しない環境で 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 の両方を記述すると、見ただけでは判断できないファイルになります。どちらの値が適用されたかを推測せず、デーモンに確認してください。
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に達している場合、kernelはまず、そのcgroup内で回収できるものを回収します。最初にclean page cache、次にswap可能なページを回収します。回収しても十分な空きができない場合、cgroupのOOM (out of memory) killerがコンテナ内のプロセスを1つ選び、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 killです。false 137は別の何かがSIGKILLを送信したことを示します。通常の原因は、アプリがSIGTERMを無視し、docker compose stopが10秒間の猶予期間に達したことです。この違いを見分けると、2つの問題には共通点がないため、調査時間を大幅に短縮できます。
イベントを記録する場所は、ほかに2つあります。daemonをリアルタイムで監視します。
docker events --filter event=oom次にkernel logを読みます。これはrestart後も残る記録です。
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 prefixのない行はhost OOMです。これはマシン自体のRAMが不足したことを意味します。これこそlimitsで防ぐべき障害です。この表示がある場合、limitsの合計が高すぎるか、limitが設定されていないserviceがあります。
restart: unless-stoppedを使用すると、OOM loopは見つかりにくくなります。serviceは停止した1秒後にdocker compose psでupと表示されるためです。uptime columnとrestart countを確認してください。また、limitとアプリをunhealthyとして報告するhealthcheckを組み合わせてください。そうすれば、監視していなくても、繰り返し停止するコンテナを確認できます。
Reservation はヒントであり、制限がルールです
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より大きい場合、起動後にカーネルがこのコンテナ内の何かを kill しています。max カウンターが増加している場合、コンテナは現在上限に達した状態で動作しています。どちらのコマンドにも、イメージ内の shell と coreutils が必要です。そのため、distroless イメージや scratch イメージでは失敗します。
8GB VPSのサイズ制限
アプリケーションではなく、まずホストから始めます。8GB VPSでは、kernel、Docker daemon、sshd、journald、自分のログインシェルのために約1GBを残します。利用できるのはおよそ7GBなので、すべてのコンテナの制限値の合計はこの範囲内に収めます。過剰な割り当ては、2つのサービスが同時にピークに達する日まで機能します。
8GBのホストでの現実的な分割例:
- Reverse proxy: 128mの制限。小規模なプロセスなので、この厳しい制限によって、暴走した設定の再読み込みをすぐに検出できます。
- PostgreSQL: 2gの制限。データベース設定で
shared_buffersを約512MBに設定します。 - Application container: 1gの制限。
- Background worker: 512mの制限。
- Mediaまたはfile service: 2gの制限。その大部分はpage cacheに使用されます。
これらの数値を自分のstackにそのまま適用しないでください。実際の負荷をかけてサービスを1日稼働させ、docker statsを監視します。各コンテナのanonのピーク値を取得し、その約半分を余裕として加えます。制限が厳しすぎると、制限なしの場合より悪い結果になります。通常のネットワークトラフィックの急増時に、正常なサービスを停止させるためです。
もう1つ注意すべき点があります。制限を知らせない限り、ほとんどのruntimeはその制限を認識しません。PostgreSQLはshared_buffersとwork_memをコンテナの制限を超えて設定できるため、プロセスが強制終了されます。JVM (Java virtual machine)では、ホストのRAMではなくcgroupの制限値に基づいてheapのサイズを決めるために-XX:MaxRAMPercentage=75が必要です。Node.jsでは、コンテナの制限値より低い値をメガバイト単位で--max-old-space-sizeに設定する必要があります。設定しないと、garbage collectorによってheapが拡大し続け、最終的にkernelが介入します。cgroupは調整しません。プロセスを強制終了します。
CPU制限の動作はまったく異なります
cpus: "1.5"は1つのコアの150%を意味し、CFS(完全公平スケジューラ)のクォータとして適用されます。コンテナは100msの各期間で150msのCPU時間を使用でき、その時間はすべてのスレッドで共有されます。使い切ると、kernelは次の期間まで待機させます。
ここが重要な違いです。メモリ制限を超えたコンテナは強制終了されます。CPU制限を超えたコンテナはスロットリングされ、処理速度は低下しますが動作を続けます。そのため、CPU制限は余裕を少なく設定しても安全ですが、メモリ制限には余裕が必要です。
cpu_sharesは別の機能です。これは、CPUが実際に飽和している場合にだけ作用する相対的な重みです。sharesが1024のコンテナと512のコンテナは、負荷の高い1つのコアをおよそ2対1に分けて使用します。アイドル状態のホストでは、どちらも制限されません。sharesを使用してサービスの重要度を順位付けし、実際の上限が必要な場合はcpusを使用します。たとえば、夜間のtranscodeジョブによってweb serverが処理能力を奪われるのを防げます。
FAQ
deploy.resources.limits は Docker Swarm なしで動作しますか
はい。単一ホストで docker compose up を実行すると、Compose V2 は deploy.resources.limits と deploy.resources.reservations を適用します。docker inspect --format '{{.HostConfig.Memory}}' <container> で確認できます。このコマンドは制限値をバイト単位で表示し、制限が適用されていない場合は 0 を表示します。deploy 内で Swarm を実際に必要とするキーは、mode、placement、update_config、endpoint_mode です。
Docker Compose の終了コード 137 は何を意味しますか
メインプロセスが SIGKILL を受信したことを意味します。137 は 128 にシグナル番号 9 を加えた値です。一般的な原因は kernel 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 を維持してください。1つのサービスで両方を設定するとファイルが読みにくくなるだけなので、どちらか1つを選び、docker inspect で結果を確認してください。
コンテナが kill されずにメモリ上限いっぱいの状態なのはなぜですか
docker stats の使用量にはページキャッシュが含まれます。kernel は OOM kill を実行する代わりに、負荷がかかるとページキャッシュを破棄します。docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat を実行し、回収できないワーキングセットを示す anon の値を確認してください。file の値が高く、anon の値が低い場合、コンテナはディスク入出力を実行しています。コンテナがまもなく停止する状態ではありません。
8GB VPS では、どの程度の RAM を未割り当てにすべきですか
kernel、Docker daemon、sshd、journald、自分の shell 用に約 1GB を残し、すべてのコンテナの制限値の合計を残りの 7GB 未満にしてください。数値を確定する前に、実際の負荷を1日かけて各コンテナのピーク anon 値を監視してください。合計値は満たすべき目標ではなく、予算として扱ってください。