Docker Compose 記憶體限制設定,避免 OOM
用 Docker Compose 設定 deploy.resources 或 mem_limit,限制單一容器的記憶體與 CPU,避免 VPS 被拖垮;並說明 exit 137、swap 與容量估算。
Docker Compose 記憶體限制的作用
Docker Compose 記憶體限制是 Linux kernel 對單一容器的 cgroup(control group;用來管理一組程序資源的 kernel 功能)設定的硬性上限。在服務上設定 deploy.resources.limits.memory 後,該容器使用的記憶體就不可能超過指定數值。容器嘗試超過上限時,kernel 會終止容器內的程序,而容器通常會以 code 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 file,可參考 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 時,Docker Compose 的 docker compose plugin 會套用 deploy.resources.limits 和 deploy.resources.reservations,完全不需要 Swarm cluster。該 plugin 目前屬於 v5 系列(截至 2026-10-10 查詢時,最新穩定版本為 v5.6.0)。它從 v2 直接跳到 v5,略過 3 和 4,以避免與舊版 Compose 檔案格式版本混淆;而且沒有任何 v5 版本變更資源限制的處理方式。deploy 區塊中以 Swarm 為導向的部分是其他鍵,例如 placement、update_config 和 endpoint_mode。這些鍵會為 docker stack deploy 說明跨節點 cluster 的排程與 rollout。因此,常見的「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,核心會先回收該 cgroup 內可回收的內容:先回收乾淨的頁面快取,再回收可交換出去的頁面。如果回收的記憶體仍不足,cgroup 的 OOM(out of memory)killer 會選取容器內的一個程序,並向它傳送 SIGKILL。終止容器的 PID 1 會結束容器。結束代碼 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(程式也可以刻意以 137 結束,但這種情況很少見),而最常見的原因是 docker compose stop 到達 10 秒的寬限期,因為應用程式忽略了 SIGTERM。這項區分可以節省數小時,因為這兩個問題完全無關。
另外還有兩個位置會記錄此事件。即時監看 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) 開頭的訊息。這表示某個容器達到了自己的限制,即使主機仍有大量可用 RAM,也可能出現這行訊息。沒有 Memory cgroup 前綴的訊息表示主機 OOM,也就是整台機器耗盡了 RAM。限制的目的正是防止這種故障,因此看到這種訊息通常表示所有限制的總和過高、部分服務完全沒有設定限制,或 Docker 外部的某個元件使用了超出預留餘裕的資源。
使用 restart: unless-stopped 時,OOM 迴圈很容易被掩蓋,因為服務在終止後一秒就會顯示為 docker compose ps。請檢查 uptime 欄位與重新啟動次數,並搭配會回報應用程式狀態為不健康的 healthcheck,如此一來,即使不持續監看,也能發現不斷終止的容器。
Reservation 是提示,limit 才是規則
reservations.memory(較舊的 mem_reservation)是軟性下限。Docker 將其描述為軟性限制,當 daemon 偵測到主機發生資源競爭或記憶體不足時才會啟用。它不會阻止容器超過此值,也不保證容器提出記憶體需求時一定有可用記憶體。它只會讓核心優先回收超過 reservation 的容器所使用的記憶體。
因此,reservation 本身無法提供任何保護。可用它標示希望在資源壓力下獲得優先處理的服務,並依靠 limit 確保安全。reservation 必須低於 limit,否則容器將無法啟動:Docker 會以 Minimum memory limit can not be less than memory reservation limit 拒絕此設定。
Swap 計算說明
大多數 VPS 映像檔完全不含 swap 檔案。執行 swapon --show 和 free -h。如果 swap 總量為零,以下所有與 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,能更快且更可預期地失敗。
記憶體使用量看起來比實際情況嚴重
在 Linux 上,docker stats 中的 MEM USAGE 數值只會扣除非活動頁面快取(cgroup v2 中為 inactive_file),因此最近使用過的快取仍會計入。讀取大型檔案的容器會逐漸接近其限制,並維持在該數值附近。這是正常現象,不代表記憶體洩漏,因為在呼叫 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 給 kernel、Docker daemon、sshd、journald 及自己的登入 shell。剩下約 7GB 可分配,所有容器限制的總和都應低於這個數值。過度配置在兩個服務同時達到尖峰之前都可能正常運作。
在 8GB 主機上,可採用以下分配方式:
- 反向代理:限制為 128m。這類程序通常很小,設定如此嚴格的限制,可立即發現失控的設定重新載入。
- PostgreSQL:限制為 2g,並在資料庫設定中將
shared_buffers設為約 512MB。 - 應用程式容器:限制為 1g。
- 背景工作程序:限制為 512m。
- 媒體或檔案服務:限制為 2g,其中大部分會用於 page cache。
不要直接將這些數值套用到自己的服務堆疊。讓服務在實際負載下執行一天,監控 docker stats,記錄每個容器的 anon 峰值,再額外增加約一半作為餘裕。限制設得過緊比不設限制更糟,因為正常的流量尖峰就可能終止健康的服務。
有一個陷阱值得個別說明。某些 runtime 除非明確告知限制,否則會忽略這些限制;即使會讀取限制的 runtime,也不一定會依照預期調整自身大小。PostgreSQL 會將 shared_buffers 和 work_mem 設定到超過容器限制,最後遭到終止。現代 JVM (Java virtual machine) 預設會偵測 cgroup 限制,但 OpenJDK 等 HotSpot 建置版本只會將 heap 設為限制的 25 percent。因此,通常應使用 -XX:MaxRAMPercentage=75,讓 heap 使用更多限制,同時為 heap 外的記憶體保留空間。自 version 12 起,Node.js 會依據 cgroup 限制設定預設 heap 大小,約為容器限制的一半,並在約 2GB 時趨於穩定。因此,只有在需要不同上限時,才以 megabytes 設定 --max-old-space-size;其值應低於容器限制,讓 garbage collector 能在 kernel 介入前運作。Ollama 的情況相同,但使用不同的設定,因為 提高 num_ctx 會讓 KV cache 增加數百 megabytes,容器可能在長提示的中途終止。cgroup 不會協調。它會直接終止程序。
CPU 限制的運作方式完全不同
cpus: "1.5" 表示單一核心的 150%,由 CFS(completely fair scheduler)配額強制執行。容器在每個 100ms 的週期中可使用 150ms 的 CPU 時間,且由容器內的所有執行緒共用。配額用完後,kernel 會讓容器等待下一個週期。
這是重要的差異。超過記憶體限制的容器會被終止。超過 CPU 限制的容器則會遭到節流,仍可繼續執行,但速度較慢。因此,CPU 限制可以設得較為積極;記憶體限制則需要預留餘裕。
cpu_shares 是不同的工具:它是相對權重,只有在 CPU 確實飽和時才會生效。兩個 shares 分別為 1024 和 512 的容器,會大致以 2:1 的比例分配繁忙的核心;在閒置的主機上,兩者都不會受到限制。使用 shares 排定服務的重要性;需要設定實際上限時則使用 cpus,例如避免夜間轉碼工作耗盡資源,導致 Web 伺服器無法正常服務。
FAQ
deploy.resources.limits 不使用 Docker Swarm 也能運作嗎?
可以。Docker Compose 的 docker compose plugin(目前為 v5 系列)會在單一主機上執行 docker compose up 時套用 deploy.resources.limits 與 deploy.resources.reservations。執行 docker inspect --format '{{.HostConfig.Memory}}' <container> 即可確認;該指令會以 bytes 顯示限制,若未套用限制則顯示 0。deploy 內以 Swarm 為使用情境所撰寫的設定鍵,指的是叢集相關設定,例如 placement、update_config 與 endpoint_mode。
Docker Compose 中的 exit code 137 代表什麼?
這幾乎總是表示主要程序收到 SIGKILL,因為 137 等於 128 加上 signal 9。(程式也可能刻意以 137 結束,但這種情況很少見。)常見原因是 kernel 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。Compose Specification 要求兩者同時設定時必須一致,因此請選擇其中一種,並使用 docker inspect 驗證結果。
為什麼容器使用完整記憶體限制,卻沒有被終止?
docker stats 中的使用量仍會計入最近使用過的 page cache。系統承受記憶體壓力時,kernel 會先丟棄這些快取,而不會觸發 OOM kill。執行 docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat 並查看 anon 值;這代表無法回收的 working set。file 值偏高、而 anon 值偏低,表示容器正在進行磁碟輸入與輸出,不是即將停止的容器。
在 8GB VPS 上,我應保留多少 RAM 不配置?
為 kernel、Docker daemon、sshd、journald 及自己的 shell 保留約 1GB,然後將所有容器限制的總和維持在剩餘 7GB 以下。在決定數值前,先在實際負載下觀察每個容器一天的峰值 anon 值,並將總量視為預算,而不是必須填滿的目標。