Docker Compose 記憶體限制與 OOM 排查
了解如何在 Docker Compose 設定記憶體與 CPU 上限,避免單一容器拖垮 VPS,並釐清 deploy.resources、mem_limit、exit 137、swap 與容量估算。
Docker Compose 記憶體限制的作用
Docker Compose 記憶體限制是 Linux kernel 對單一容器 cgroup(control group,用於為一組程序計量資源的 kernel 功能)設定的硬上限。在服務上設定 deploy.resources.limits.memory 後,該容器使用的記憶體不會超過指定數值。容器嘗試超過限制時,kernel 會終止容器內的程序,而容器通常會以代碼 137 結束。
這在 VPS 上尤其重要,因為 VPS 的 RAM 是固定的,沒有多餘的主機記憶體可供借用。某個容器發生記憶體洩漏或執行錯誤查詢時,可能會耗盡 8GB 主機上的所有可用頁面。接著 kernel 會終止它判定最適合終止的程序,通常是資料庫或你的 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 目前讀取的格式。
兩者都能在單一主機上運作。執行 docker compose up 時,Compose V2,也就是 docker compose 外掛程式,會套用 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 而達到 10 秒的寬限期。區分這兩種情況可以節省數小時,因為這兩個問題毫無關聯。
另外還有兩個位置會記錄此事件。即時監看 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 迴圈很容易被掩蓋,因為服務在程序終止 1 秒後就會在 docker compose ps 顯示為正常運作。請檢查 uptime 欄位與重新啟動次數,並搭配 回報應用程式為不健康的 healthcheck 設定限制,這樣即使不持續監看,也能發現不斷終止的容器。
Reservation 是提示,limit 才是規則
reservations.memory(較舊的 mem_reservation)是 soft floor。Docker 將其描述為一種 soft limit,當 daemon 偵測到主機發生資源爭用或記憶體不足時才會啟用。它不會阻止 container 使用超過此值的記憶體,也不保證 container 發出記憶體需求時一定有可用記憶體。它只會讓 kernel 優先從超過 reservation 的 container 回收資源。
因此,reservation 本身沒有任何保護作用。可用它標記希望在資源壓力下獲得優先處理的服務,安全性則應依賴 limit。reservation 必須低於 limit,否則 container 將無法啟動: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。將這兩個值設為相同,容器就完全沒有 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 的限制,會更快且更可預測地失敗。
Why memory usage looks worse than it is
The MEM USAGE figure in docker stats includes page cache, so a container that reads large files climbs toward its limit and sits there. That is normal, and it is not a leak, because clean cache is reclaimed before the OOM killer is ever called. A service like a self-hosted Jellyfin media server will look permanently near its ceiling for exactly this reason.
Split the number into cache and real working set from inside the container:
docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.eventsanon is anonymous memory, the working set that cannot be dropped. file is page cache, which can. Size your limit against anon plus a margin, not against the total. The memory.events file settles the argument outright: an oom_kill counter above zero means the kernel has killed something in this container since it started, and a max counter climbing means the container is being held at its ceiling right now. Both commands need a shell and coreutils inside the image, so they fail on a distroless or scratch image.
8GB VPS 的資源限制
從主機開始規劃,不要先從應用程式著手。在 8GB VPS 上,預留約 1GB 給 kernel、Docker daemon、sshd、journald 及自己的登入 shell。剩餘約 7GB 可分配給服務,而且所有容器限制的總和應低於這個數值。過度分配在兩個服務同時達到峰值的那一天之前,通常都不會出問題。
以下是 8GB 主機的一種可行配置:
- 反向代理:限制為 128m。這類程序很小,限制設得這麼緊,可立即發現失控的設定重新載入。
- PostgreSQL:限制為 2g,並在資料庫設定中將
shared_buffers設為約 512MB。 - 應用程式容器:限制為 1g。
- 背景工作程序:限制為 512m。
- 媒體或檔案服務:限制為 2g,其中大部分會用作 page cache。
不要直接將這些數值套用到自己的服務堆疊。讓服務在實際負載下執行一天,監控 docker stats,記錄每個容器的 anon 峰值,再額外增加約一半作為預留空間。限制設得過緊比不設限制更糟,因為正常的流量尖峰就可能終止原本健康的服務。
有一個陷阱值得單獨說明。除非明確告知多數 runtime,否則它們看不到這項限制。PostgreSQL 會直接將 shared_buffers 和 work_mem 設定到超過容器限制,最後遭到終止。JVM(Java virtual machine)需要 -XX:MaxRAMPercentage=75,才能依據 cgroup 限制,而不是主機 RAM 設定其 heap。Node.js 需要以 MB 為單位的 --max-old-space-size,且數值應低於容器限制,否則其 garbage collector 會讓 heap 持續成長,直到 kernel 介入。Ollama 的情況相同,但使用不同的設定,因為 提高 num_ctx 會讓 KV cache 增加數百 MB,容器會在長提示詞處理到一半時終止。cgroup 不會協商,只會終止程序。
CPU 限制的運作方式完全不同
cpus: "1.5" 表示單一核心的 150%,由 CFS(completely fair scheduler)配額強制執行。容器在每個 100ms 的週期內可使用 150ms 的 CPU 時間,且由其所有執行緒共用。用完配額後,核心會讓容器等待下一個週期。
這是重要的差異。超過記憶體限制的容器會被終止。超過 CPU 限制的容器則會受到節流,仍會繼續執行,但速度較慢。因此,CPU 限制可以設得較為積極;記憶體限制則需要保留餘裕。
cpu_shares 是不同的工具:它是相對權重,只有在 CPU 確實飽和時才會生效。兩個 shares 分別為 1024 和 512 的容器,會大致以二比一的比例分配繁忙的核心;在閒置的主機上,兩者都不會受到限制。使用 shares 排定服務的重要性;需要設定實際上限時,則使用 cpus,例如避免夜間轉碼工作耗盡資源,導致 Web 伺服器無法取得 CPU。
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。常見原因是核心的 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 值,再決定固定數值。請將總量視為預算,而不是必須填滿的目標。