SSD Nodes Learn 8GB 内存 — 每年 $66
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-01

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

在 Docker Compose 中设置内存和 CPU 上限,防止单个容器拖垮 VPS。涵盖 deploy.resources、mem_limit、退出码 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 schema,现在已成为 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,会让人无法一眼看出最终使用了哪个值。不要猜测哪个数值生效,直接向守护进程查询:

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 由其他原因发送,通常是 docker compose stop 达到其 10 秒宽限期,因为应用忽略了 SIGTERM。这个区别可以节省数小时,因为这两个问题完全无关。

还有两个位置会记录此事件。实时监控守护进程:

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 循环很容易被隐藏,因为服务会在终止一秒后显示为 docker compose ps。检查运行时间列和重启次数,并将该限制与报告应用不健康状态的 healthcheck结合使用。这样,即使你没有持续监控,反复终止的容器也能被发现。

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

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

因此,预留值本身不能提供保护。可以使用它标记希望在资源压力下得到优先照顾的服务,但安全性应依赖限制值。预留值必须低于限制值,否则容器将无法启动: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 来自使用 cgroup v1 且启动时未设置 swapaccount=1 的主机。在这些主机上,内存限制仍然生效,但 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 上,为内核、Docker daemon、sshd、journald 和您自己的登录 shell 预留约 1GB。这样约剩 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 需要将 --max-old-space-size 设置为低于容器限制的兆字节数,否则其垃圾回收器会让堆持续增长,直到内核介入。cgroup 不会协商。它会终止进程。

CPU 限制的行为完全不同

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

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

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

FAQ

deploy.resources.limits 在没有 Docker Swarm 时是否有效?

有效。在单个主机上运行 docker compose up 时,Compose V2 会应用 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 上,我应该保留多少未分配的 RAM?

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