Docker Compose 記憶體限制設定,避免 OOM
在 Docker Compose 設定記憶體與 CPU 上限,避免單一容器拖垮 VPS;涵蓋 deploy.resources、mem_limit、exit 137、swap 與容量估算。
Docker Compose 記憶體限制的作用
Docker Compose 記憶體限制是 Linux 核心套用至單一容器 cgroup(控制群組;核心用來為一組程序計量資源的功能)的硬性上限。為服務設定 deploy.resources.limits.memory 後,該容器使用的記憶體不得超過指定數值。容器嘗試超過上限時,核心會終止容器內的程序,而容器通常會以代碼 137 結束。
這在 VPS 上尤其重要,因為 RAM 是固定的,主機沒有多餘記憶體可供借用。具有記憶體洩漏或執行錯誤查詢的容器,可能耗盡 8GB 主機上的所有可用頁面。接著,核心會終止它判定狀況最差的程序;該程序通常是資料庫或 SSH 工作階段,而不是造成問題的容器。設定限制可將整台伺服器中斷縮小為單一服務重新啟動。
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:哪一個適用
同一個概念有兩種寫法,因此容易造成混淆。
mem_limit、mem_reservation、memswap_limit、cpus 和 cpu_shares 是從較舊 Compose 檔案格式繼承而來的服務頂層金鑰。deploy.resources 源自 Swarm schema,現在已成為 Compose Specification 的一部分,而這正是 docker compose 目前讀取的格式。
兩者都可在單一主機上運作。Compose V2 的 docker compose 外掛程式會在執行 docker compose up 時套用 deploy.resources.limits 和 deploy.resources.reservations,不需要任何 Swarm 叢集。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記憶體值的單位是 bytes,因此 1g 會輸出為 1073741824。CPU 的單位是 nano CPUs,因此 1.5 會輸出為 1500000000。任何欄位中的 0 都表示未設定限制。Docker 接受的最小記憶體限制是 6m,低於此值時容器會拒絕啟動。
容器達到限制時會發生什麼
容器不會變慢。它會終止。
當程序要求配置頁面,而 cgroup 已達到其 memory.max 時,核心會先回收該 cgroup 內可回收的資源:先回收乾淨的頁面快取,再回收可交換出去的頁面。如果回收的資源不足,cgroup OOM(記憶體不足)終止程式會選取容器內的一個程序,並向其傳送 SIGKILL。終止容器的 PID 1 會結束容器。結束代碼 137 只是 128 加上訊號 9,因此 137 是任何 SIGKILL 的特徵,不單獨代表 OOM。
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 是 OOM 終止。false 137 表示 SIGKILL 是由其他來源傳送,通常原因是 docker compose stop 因應用程式忽略 SIGTERM 而達到十秒的寬限期。這項區分可以節省數小時,因為這兩個問題完全無關。
另外有兩個位置會記錄此事件。即時監控 daemon:
docker events --filter event=oom接著讀取核心日誌,這是重新啟動後仍會保留的記錄:
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'cgroup 終止事件會輸出以 Memory cgroup out of memory: Killed process 24713 (node) 開頭的行。沒有 Memory cgroup 前綴的行表示主機 OOM,也就是整台機器耗盡 RAM。限制的目的就是防止這種故障,因此看到這種情況表示你的限制總和過高,或某些服務完全未設定限制。
使用 restart: unless-stopped 時,OOM 迴圈很容易被掩蓋,因為服務會在程序終止一秒後於 docker compose ps 顯示為運作中。請檢查 uptime 欄和重新啟動計數,並搭配 將應用程式回報為狀況不良的健康檢查,讓持續終止的容器無須持續監看也能被發現。
Reservation 是提示,limit 才是規則
reservations.memory(較舊的 mem_reservation)是軟性下限。Docker 將其描述為軟性限制,當 daemon 偵測到主機上的資源爭用或記憶體不足時便會啟用。它不會阻止 container 超過此值,也不會保證 container 發出要求時一定有可用記憶體。它只會讓 kernel 優先從超過 reservation 的 container 回收資源。
因此,reservation 本身不提供任何保護。您可以使用它標示希望在資源壓力下獲得優先照顧的服務,並依賴 limit 確保安全。請將 reservation 設定低於 limit,否則 container 將無法啟動:Docker 會以 Minimum memory limit can not be less than memory reservation limit 拒絕此設定。
Swap 使用量:如實說明
大多數 VPS 映像檔完全沒有 swap 檔案。執行 swapon --show 和 free -h。如果 swap 總量為 0,以下所有與 swap 相關的設定都不會生效,記憶體限制就是單純的 RAM 上限。
memswap_limit 不是 swap 的容量,而是記憶體加上 swap 的總量。使用 mem_limit: 1g 和 memswap_limit: 2g 時,容器會取得 1GB RAM 和 1GB swap。將這兩個值設為相同,代表容器完全沒有 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 的作用。它會讓 OOM kill 變慢,而不是降低發生機率,因為有記憶體洩漏的程序填滿 swap 的速度,與填滿 RAM 一樣快。同時,在共用 VPS 儲存空間上持續大量使用 swap 的容器,會拖慢該主機上的其他服務。對任何對延遲敏感的工作而言,設定正確且不使用 swap 的限制,會更快且更可預測地失敗。
記憶體使用量看起來比實際情況更糟
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 計數器持續增加,表示容器目前正被限制在其上限。這兩個命令都需要映像檔內有 shell 和 coreutils,因此在 distroless 或 scratch 映像檔上會失敗。
8GB VPS 上的資源配置限制
請從主機開始規劃,而不是從應用程式開始。在 8GB VPS 上,請保留約 1GB 給核心、Docker daemon、sshd、journald 以及您自己的登入 shell。如此約有 7GB 可分配,且所有容器限制的總和應低於此數值。過度分配可能一直運作,直到某天有 2 個服務同時達到尖峰。
在 8GB 主機上可採用以下配置:
- 反向 Proxy:限制為 128m。這是小型程序,如此嚴格的限制可立即發現失控的設定重新載入。
- PostgreSQL:限制為 2g,並在資料庫設定中將
shared_buffers設為約 512MB。 - 應用程式容器:限制為 1g。
- 背景工作程序:限制為 512m。
- 媒體或檔案服務:限制為 2g,其中大部分會用於頁面快取。
請勿直接將這些數值套用到您自己的堆疊。讓服務在實際負載下執行 1 天,監控 docker stats,取得每個容器的 anon 峰值,然後再增加約一半作為餘裕。限制設得過緊比不設限制更糟,因為正常的網路流量尖峰可能會導致正常服務遭到終止。
有一個陷阱值得個別說明。除非明確告知,多數執行環境都看不到這項限制。PostgreSQL 會直接將 shared_buffers 和 work_mem 配置到超過容器限制的大小,最後遭到終止。JVM (Java virtual machine) 需要 -XX:MaxRAMPercentage=75,才能根據 cgroup 限制,而不是主機 RAM 配置其 heap。Node.js 需要以 MB 為單位的 --max-old-space-size,且數值必須低於容器限制,否則其垃圾收集器會讓 heap 持續增長,直到核心介入。cgroup 不會協商,而是直接終止程序。
CPU 限制的行為完全不同
cpus: "1.5" 表示單一核心的 150%,並由 CFS(completely fair scheduler)配額強制執行。容器在每個 100ms 的週期內可使用 150ms 的 CPU 時間,且由其所有執行緒共用。用完這段時間後,核心會讓容器等待下一個週期。
這是兩者的重要差異。超過記憶體限制的容器會被終止。超過 CPU 限制的容器則會受到節流,仍可繼續執行,但速度較慢。因此,可以較積極地設定 CPU 限制;記憶體限制則需要保留餘裕。
cpu_shares 是不同的工具:它是相對權重,只有在 CPU 確實飽和時才會生效。share 分別為 1024 和 512 的兩個容器,會以約二比一的比例分配繁忙的核心;在閒置的主機上,兩者都不會受到限制。使用 share 依服務的重要性排序;需要實際上限時,則使用 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> 進行確認;此命令會以 bytes 顯示限制,若未套用限制則顯示 0。deploy 內真正需要 Swarm 的鍵為 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 表示因記憶體不足而終止,false 137 則不是。
我應該設定 mem_limit,還是 deploy.resources.limits.memory?
搭配 docker compose 時,兩者皆可使用。deploy.resources.limits.memory 是目前 Compose Specification 的寫法,也是新檔案較佳的預設選擇。如果檔案其他部分已使用較舊的頂層鍵,請保留 mem_limit。在同一個服務上同時設定兩者只會降低檔案可讀性,因此請擇一使用,並透過 docker inspect 驗證結果。
為什麼容器使用完整記憶體限制,卻沒有被終止?
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 以下。在實際負載下觀察每個容器每天的峰值 anon 值,再決定固定的數值;請將總量視為預算,而不是必須填滿的目標。