SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-10-11

Docker Compose 内存限制设置,避免容器 OOM

了解 Docker Compose 中 deploy.resources 与 mem_limit 的区别,验证内存上限,处理退出码 137,并合理配置 CPU、swap 和容器内存。

Docker Compose 内存限制的作用

Docker Compose 内存限制是 Linux 内核对单个容器的 cgroup(控制组,即用于统计一组进程资源使用情况的内核功能)设置的硬上限。为服务设置 deploy.resources.limits.memory 后,该容器的内存使用量不会超过指定值。超过限制时,内核会终止容器内的某个进程,容器通常会以退出码 137 退出。

这在 VPS 上尤其重要,因为 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-stream

MEM 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 的 docker compose 插件在运行 docker compose up 时会应用 deploy.resources.limits 和 deploy.resources.reservations,不需要任何 Swarm 集群。该插件目前处于 v5 系列(截至 2026-10-10 检查时,最新稳定版本为 v5.6.0)。它从 v2 直接跳到 v5,跳过了 3 和 4,以避免与旧版 Compose 文件格式的版本号混淆;没有任何 v5 版本改变资源限制的处理方式。deploy 块中面向 Swarm 的部分是其他键,例如 placement、update_config 和 endpoint_mode。这些键用于描述 docker stack deploy 的节点集群中的调度和滚动更新。因此,对于 resources 子部分,“deploy 需要 Swarm”的常见说法是错误的。遵循这一说法会导致服务完全没有资源限制。

每个项目只选择一种写法。在同一个服务中同时写入 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

内存值的单位是字节,因此 1g 会显示为 1073741824。CPU 使用 nano CPU,因此 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-1

true 137 表示发生了 OOM 终止。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 终止会输出一行以 Memory cgroup out of memory: Killed process 24713 (node) 开头的日志。这表示某个容器达到了自己的限制,即使主机仍有大量可用 RAM,也可能出现这行日志。不带 Memory cgroup 前缀的日志表示主机发生了 OOM,即整台机器耗尽了 RAM。这正是限制要防止的故障,因此出现此情况通常意味着限制总和过高、某些服务完全没有设置限制,或者 Docker 外部的某个进程使用了超过预留余量的内存。

使用 restart: unless-stopped 时,OOM 循环很容易被隐藏,因为服务在退出后 1 秒就会在 docker compose ps 中显示为运行状态。检查 uptime 列和重启次数,并将限制与 报告应用不健康状态的运行状况检查 配合使用。这样,即使不持续监控,也能发现反复退出的容器。

预留是提示,限制才是规则

reservations.memory(较旧的 mem_reservation)是软下限。Docker 将其描述为一种软限制:当 daemon 检测到主机存在资源争用或内存不足时,该限制才会生效。它不会阻止容器超过该值,也不会保证容器请求内存时一定有可用内存。它只会让内核优先从超过预留值的容器回收资源。

因此,预留值本身不能提供任何保护。使用它标记希望在资源压力下获得优先照顾的服务,并依靠限制值确保安全。预留值必须低于限制值,否则容器将无法启动:Docker 会拒绝包含 Minimum memory limit can not be less than memory reservation limit 的配置。

交换空间计费,实事求是

大多数 VPS 镜像完全不提供交换文件。运行 swapon --show 和 free -h。如果交换空间总量为零,下面所有与交换空间相关的设置都不会生效,此时内存限制就是纯 RAM 上限。

memswap_limit 不是交换空间的大小,而是内存和交换空间的总和。使用 mem_limit: 1g 和 memswap_limit: 2g 时,容器将获得 1GB RAM 和 1GB 交换空间。将这两个值设为相同值,容器就完全没有交换空间。设置 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 存储上频繁使用交换空间的容器会拖慢主机上的所有其他服务。对于任何对延迟敏感的服务,设置正确的限制并禁用交换空间,可以更快且更可预测地失败。

内存使用量看起来比实际更高的原因

在 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.events

anon 是匿名内存,即无法丢弃的工作集。file 是页缓存,可以被回收。应根据 anon 加上余量设置限制,而不是根据总量设置。memory.events 文件可以直接给出结论:oom_kill 计数器大于 0,表示该容器自启动以来内核已在其中杀死过进程;max 计数器持续增加,表示容器当前正被限制在内存上限。这两个命令都需要镜像内包含 shell 和 coreutils,因此在 distroless 或 scratch 镜像中会失败。

8GB VPS 上的资源限制

从主机开始,而不是从应用开始。在 8GB VPS 上,为内核、Docker daemon、sshd、journald 和您自己的登录 shell 预留约 1GB。这样可分配的内存约为 7GB,所有容器限制的总和应低于这个数值。超额分配在两个服务同时达到峰值之前通常都能正常工作。

在 8GB 主机上,可以采用以下分配方式:

  • 反向代理:限制为 128m。它是一个小型进程,这样严格的限制可以立即发现失控的配置重新加载。
  • PostgreSQL:限制为 2g,并在数据库配置中将 shared_buffers 设置为约 512MB。
  • 应用容器:限制为 1g。
  • 后台 worker:限制为 512m。
  • 媒体或文件服务:限制为 2g,其中大部分将用于页缓存。

不要直接将这些数值复制到您自己的服务栈中。在实际负载下运行这些服务一天,监控 docker stats,记录每个容器的峰值 anon,然后在此基础上再增加约一半作为余量。限制设置得过紧比不设置限制更糟糕,因为正常的流量峰值就可能导致健康的服务被终止。

有一个常见问题值得单独说明。除非明确告知某些运行时,否则它们会忽略该限制;即使能够读取限制的运行时,也未必会按您预期的方式配置自身。PostgreSQL 会将 shared_buffers 和 work_mem 设置到超过容器限制的数值,随后被终止。现代 JVM(Java 虚拟机)默认会检测 cgroup 限制,但 OpenJDK 等 HotSpot 构建版本只会将堆设置为该限制的 25%,因此通常使用 -XX:MaxRAMPercentage=75,让堆使用更多限制,同时为堆外内存留出空间。自 version 12 起,Node.js 会根据 cgroup 限制设置默认堆大小,约为容器限制的一半,并在约 2GB 处趋于稳定。因此,只有在需要不同上限时才应以 MB 为单位设置 --max-old-space-size,并将其保持在容器限制以下,以便垃圾回收器先于内核介入。Ollama 的情况相同,但使用的是不同的参数,因为 增大 num_ctx 会使 KV cache 增加数百 MB,容器可能在处理长提示词的过程中终止。cgroup 不会协商。它会终止进程。

CPU 限制的行为完全不同

cpus: "1.5" 表示单个 CPU 核心的 150%,由 CFS(完全公平调度器)配额强制执行。容器在每个 100ms 周期内可获得 150ms 的 CPU 时间,该时间由容器的所有线程共享。用完配额后,内核会让容器等待下一个周期。

这就是关键区别。超过内存限制的容器会被终止。超过 CPU 限制的容器会被限流,但仍会继续运行,只是速度更慢。因此,可以较为激进地设置 CPU 限制;而内存限制需要预留余量。

cpu_shares 是另一种工具:它表示相对权重,仅在 CPU 实际饱和时生效。两个 shares 分别为 1024 和 512 的容器会大致按二比一的比例分配繁忙的 CPU 核心;在空闲主机上,两个容器都不会受到限制。使用 shares 按重要性排列服务;需要设置实际上限时使用 cpus,例如防止夜间转码任务耗尽资源,导致 Web 服务器运行受阻。

FAQ

不使用 Docker Swarm 时,deploy.resources.limits 是否生效?

是。Docker Compose 和 docker compose 插件(目前为 v5 系列)在单台主机上运行 docker compose up 时,会应用 deploy.resources.limits 和 deploy.resources.reservations。使用 docker inspect --format '{{.HostConfig.Memory}}' <container> 可确认结果;该命令会以字节为单位输出限制值。如果未应用限制,则输出 0。deploy 中为 Swarm 设计的键属于集群配置,例如 placement、update_config 和 endpoint_mode。

Docker Compose 中的退出代码 137 表示什么?

这几乎总是表示主进程收到了 SIGKILL,因为 137 等于 128 加信号 9。(程序也可以主动以 137 退出,但这种情况很少见。)最常见的原因是内核 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 中的使用量仍会统计最近使用过的页缓存。内核会在内存压力下回收这些缓存,而不是触发 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 值,再确定具体数值;应将总量视为预算,而不是必须填满的目标。

这篇文章对您有帮助吗?