
POV:free -h 跑出來 available 逼近見底、swap 全滿,你等著看 kernel 會不會挑人開刀,而且十有八九不是你以為的那個容器。
昨天用真的在跑的 docker compose 檔,講 container 替我擋掉了環境漂移、port 衝突、重啟這三件事。今天講它擋不住的那一天:9 月初,我這台 GCP e2-standard-8(8 vCPU / 32 GB、Spot VM)被壓到 swap 見底,而 docker compose 裡面,看不出發生了什麼。
先講結論:沒有任何一個容器單獨出問題。 9/2 到 9/3 那兩天的壓力,是很多東西疊出來的。
常駐的部分:RAG 引擎實測約 2.19 GiB(上限 6 GiB)、Qdrant 上限 1 GiB。再加上好幾個 OMP / Claude agent session,各約 0.4–1.6 GiB。這些單看都合理,加總就不小。
短期的部分更麻煩。兩個 headless Chrome renderer 長時間各占 42–47% CPU。一個 ARM64 的 docker buildx --platform linux/arm64 透過 QEMU 跑 next build,CPU 到約 212%,記憶體持續 swap in / swap out。
這裡要分兩塊看:RAG 引擎、Qdrant 本來就在 devstack 的 docker-compose.yml 裡,是有 compose 管的容器負載;真正沒寫在任何 compose 裡的,是 agent session、Chrome renderer、QEMU build 這些主機上的 process。compose 管得到的容器大致守規矩——雖然後面會看到,閒置容器一樣能疊出壓力;真正管不到、也是這次互搶主力的,是主機上的 process。
主機記憶體逼近見底、swap 也全滿。先看容器與主機 process list:
free -h
docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}'
發現: 大多不是我當下在用的東西:
| 來源 | 記憶體 | 當下在用? |
|---|---|---|
uvicorn :4001(RAG 引擎) |
偏高但屬正常運作範圍 | 否 |
| 多個 omp session 合計 | 有一定佔比 | 部分 |
next-server |
1.25 GiB | 否 |
最後那個 next-server 追到另一個專案的 dev 容器。看一下它的設定(以下用示意容器名 idle-dev-app 代替真實名稱):
docker inspect idle-dev-app \
--format '{{.HostConfig.RestartPolicy.Name}} {{.State.StartedAt}}'
# unless-stopped 2026-09-04T12:20:...
它是另一個專案用 docker-compose.dev.yml 起的。從 9/4 12:20 起跑了 45 小時,這兩天沒有人在用它。 它沒有壞,只是很忠實地執行 restart: unless-stopped:機器重開,它就回來。

當天做了三件事,全是手動。
第一,停掉閒置容器:docker stop idle-dev-app。停完 available 從 8 GiB 回到 21 GiB。unless-stopped 的語意是明確 stop 之後會保持停止,之後它就沒有再自己回來過。
第二,發現 available 明明有 17–21 GiB,swap 4.0 GiB 卻還用了 3.9 GiB——這是壓力尖峰留下的殘留,kernel 不會主動把它搬回 RAM,只能手動 swapoff -a && swapon -a 倒一次。
第三,回頭想根因:這台機器的 RAM 從早期 8 GB、16 GB 一路升到 32 GB,但 vm.swappiness=60(預設)和 4 GB 的 swapfile 從來沒跟著調。改成 10 並持久化,swapfile 也線上換成 8 GB,不用重開機:
sudo swapoff -a && sudo swapon -a
sudo sysctl vm.swappiness=10
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-devbox-swappiness.conf
sudo swapoff /swapfile
sudo fallocate -l 8G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
沒設 mem_limit 的容器,也可以用 docker update --memory 512m <container> 在不重啟的情況下先補一個上限止血。
回頭看,docker compose 沒做錯什麼,它的邊界就到這裡:
mem_limit,但沒有地方寫「這台 32 GB,容器最多用 20 GB」。各容器的上限加總超過實體記憶體,也不會有人擋。unless-stopped 的語意陷阱。 它只回答「機器重開要不要回來」,不回答「有沒有人在用」。閒置 45 小時的 dev 容器和正在服務的 bot,在它眼裡一模一樣。記憶體 reach to limit 時,出手的是 Linux 的 OOM killer,它挑誰跟我的優先順序無關——跟前面 unless-stopped 不管「有沒有人在用」一視同仁,是同一種問題。那天我手動做的那些判斷——這個服務該分多少、見底時誰先被犧牲——在 K8s 語言裡分別有名字,Day 09 我們再繼續討論:
我沒有要說這台機器現在就該搬去 K8s(要不要搬是 Day 16 之後的題目)。我只是發現:這些判斷不該由人一直手動做——K8s 裡分別交給 requests/limits、eviction、workload 管理策略處理;排程器負責的只是把符合資源需求的 Pod 放到合適的節點上。
明天把 K8s 最核心的三個心智模型(Pod / Deployment / Service)用管 bot 艦隊的語言講一遍。
docker compose 替每個服務寫下「它需要什麼」,但卻還沒有將整個環境記錄下來 (記帳概念);帳沒人記,最後就是會遇到 OOM。
docker container update(線上調整容器資源上限)