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

Docker Compose 内存限制设置与 OOM 处理

了解如何用 deploy.resources 或 mem_limit 限制容器内存和 CPU,避免单个服务耗尽 VPS 资源,并排查退出码 137、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_limitmem_reservationmemswap_limitcpuscpu_shares 是从旧版 Compose 文件格式继承而来的顶层服务键。deploy.resources 来自 Swarm 架构,现在已成为 Compose Specification 的一部分,也是 docker compose 当前读取的格式。

两者都可在单台主机上使用。Compose V2,即 docker compose 插件,在运行 docker compose up 时会应用 deploy.resources.limitsdeploy.resources.reservations,不需要任何 Swarm 集群。deploy 块中仅供 Swarm 使用的是其他键:modeplacementupdate_configendpoint_modedocker stack deploy 有效,但会被 docker compose up 忽略。因此,“deploy 需要 Swarm”这一常见说法对于 resources 子节并不正确。遵循这一说法会导致服务完全没有限制。

每个项目只选择一种写法。在同一个服务中同时写入 mem_limit: 512mdeploy.resources.limits.memory: 1g,会让人无法一眼判断哪个值生效。不要猜测哪个数值最终生效,而应直接向 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 kill。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 kill 会输出以 Memory cgroup out of memory: Killed process 24713 (node) 开头的行。不带 Memory cgroup 前缀的行表示主机 OOM,即整台机器的 RAM 已耗尽。这正是限制要防止的故障,因此看到这种情况说明你的限制总和过高,或者某些服务完全没有设置限制。

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

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

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

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

Swap 记账,实话实说

大多数 VPS 镜像根本没有 swap 文件。运行 swapon --showfree -h。如果 swap 总量为 0,下面所有与 swap 相关的设置都不会生效,内存限制就是纯 RAM 上限。

memswap_limit 不是 swap 的大小,而是内存与 swap 的总和。使用 mem_limit: 1gmemswap_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.events

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

8GB VPS 上的容量限制

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

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

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

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

有一个问题需要单独说明。除非明确告知大多数运行时环境,否则它们无法看到该限制。PostgreSQL 会将 shared_bufferswork_mem 设置到超过容器限制的大小,随后被终止。JVM(Java 虚拟机)需要使用 -XX:MaxRAMPercentage=75,根据 cgroup 限制而不是主机 RAM 设置堆大小。Node.js 需要以 MB 为单位设置 --max-old-space-size,且该值应低于容器限制,否则其垃圾回收器会让堆持续增长,直到内核介入。Ollama 的情况相同,只是使用了不同的参数,因为 增大 num_ctx 会使 KV 缓存增加 数百 MB,容器会在长提示词处理到一半时终止。cgroup 不会协商。它会直接终止进程。

CPU 限制的行为完全不同

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

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

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

FAQ

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

可以。Compose V2 在单台主机上运行 docker compose up 时会应用 deploy.resources.limitsdeploy.resources.reservations。使用 docker inspect --format '{{.HostConfig.Memory}}' <container> 可确认结果;如果未应用限制,该命令会以字节为单位输出限制值,并输出 0deploy 中确实需要 Swarm 的键包括 modeplacementupdate_configendpoint_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 上应保留多少未分配内存?

为内核、Docker daemon、sshd、journald 和您自己的 shell 保留约 1GB,然后将所有容器限制之和控制在剩余的 7GB 以内。在确定具体数值前,应在实际负载下监控每个容器一天的峰值 anon 值,并将总量视为预算,而不是必须填满的目标。