为什么单容器能拖垮整台服务器
很多个人站长用 Docker 部署网站后都有过这样的经历:明明容器里只跑了一个博客,服务器却突然卡死,SSH 都连不上,重启一看内存被吃光、CPU 满载。原因很简单——Docker 容器默认不限制资源。容器里的进程和宿主机上的普通进程一样,可以无限申请 CPU 和内存,一个失控的容器(比如被刷了流量、代码死循环、或者 MySQL 配置了过大的缓冲池)就能把整台 VPS 拖垮,甚至牵连同机上的其他容器和宿主系统本身。
解决这个问题并不复杂:给每个容器设置合理的资源上限,让它在「笼子」里运行。本文从 docker run 参数、Compose 编排、OOM 排查三个层面,讲清楚个人站长最常用的资源限制手段,全部命令都可以直接在你的 VPS 上执行验证。
内存限制:最关键的防线
内存是个人 VPS 上最稀缺的资源,1G 或 2G 的小机器稍不注意就会被吃满。给容器限制内存用 -m 或 --memory 参数,单位支持 b、k、m、g,例如限制最多使用 512 兆内存:
docker run -d --name blog -m 512m nginx:1.25这条命令启动一个最多占用 512 兆内存的容器。当容器内所有进程的内存总和超过这个值,内核的 OOM Killer 会按照策略杀掉容器里最「重」的进程,容器因此退出或重启。可以用 --memory-reservation 设置一个软限制,内核在内存充裕时允许容器超过它,只在宿主机内存紧张时优先回收这个容器的内存:
docker run -d --name blog -m 512m --memory-reservation 256m nginx:1.25还要留意 --memory-swap 这个参数。它表示「内存 + 交换分区」的总上限,默认情况下等于内存限制的两倍。比如只写 -m 512m,容器实际可以使用 512 兆内存加 512 兆 swap。想彻底禁止容器使用 swap,把它设成和内存一样大:
docker run -d --name blog -m 512m --memory-swap 512m nginx:1.25另外两个容易踩的坑:第一,--oom-kill-disable 只能在设置了 -m 的前提下使用,否则 Docker 会直接报错;第二,Java、PHP 这类带独立内存池的程序,限制要留足余量——比如给 Java 容器 1G 限制,JVM 堆就不要再开 1G,否则进程还没跑起来就被杀。下面是一个真实案例:一台 2G 内存的 VPS 上跑 MySQL 容器,如果不加限制,MySQL 默认的 innodb_buffer_pool_size 会根据宿主机总内存自动计算(约 1.2G 左右),再加 PHP、Nginx 和系统本身,内存立刻告急。正确做法是显式限制并同步调小 MySQL 自身的缓冲池:
docker run -d --name mysql8 -m 768m \
-e MYSQL_ROOT_PASSWORD=yourpass \
mysql:8.0 --innodb-buffer-pool-size=384M \
--max-connections=50这样即使某天流量暴涨,MySQL 也不可能把宿主机内存吃光,最坏情况只是容器内报错重启,不会牵连整台服务器。
CPU 限制:防止单容器占满核心
CPU 限制有三组常用参数,含义完全不同,别混用。第一组是 --cpus,限制容器最多使用几个 CPU 核心,支持小数:
docker run -d --name blog --cpus 0.5 nginx:1.25上面表示容器最多使用半个核心的算力,适合绝大多数个人站的小流量场景。第二组是 --cpu-shares,它是相对权重而不是绝对值:默认所有容器权重都是 1024,把某个容器设成 512,意味着 CPU 紧张时它只能分到别人的一半。机器空闲时容器仍然可以用满 CPU,所以 --cpu-shares 适合「多个容器抢资源时保证重点容器优先」,不适合当硬上限。第三组是 --cpuset-cpus,把容器绑定到指定编号的物理核心上,例如双核机器只让容器跑在 0 号核心:
docker run -d --name blog --cpuset-cpus 0 nginx:1.25对个人站长来说,最实用的组合是:核心业务容器用 --cpus 设硬上限,后台任务容器(比如定时备份、日志清理)用 --cpu-shares 降低优先级,避免它们和大流量时段抢 CPU。限制磁盘 IO 可以用 --blkio-weight,原理和 cpu-shares 类似,也是相对权重,小机器上意义不大,这里不展开。
进程数限制:防 fork 炸弹
还有一个容易被忽视的参数 --pids-limit,它限制容器内最多能同时存在的进程数量。PHP-FPM、Java 这类多进程程序动辄几十个进程,但如果代码有 bug 或者被入侵者执行了 fork 炸弹,进程数会指数级暴涨,瞬间耗尽宿主机的 PID 空间和内存。给容器加上进程数上限是性价比极高的安全措施:
docker run -d --name php-app --pids-limit 256 php:8.3-fpm一般 PHP-FPM 容器设为 128 到 256 就足够,普通 Nginx 容器 64 足矣。设置后可以用下面的命令实时观察容器到底创建了多少进程:
docker stats --no-streamdocker stats 会输出每个容器的 CPU、内存、网络和磁盘占用,是日常检查资源使用情况的第一工具。注意 stats 里的内存列显示的是当前使用量,如果容器被 OOM 杀掉,需要配合下面的排查方法确认。
容器被 OOM 杀掉怎么排查
容器突然退出、网站打不开,先看退出状态码:
docker inspect -f '{{.State.OOMKilled}}' blog返回 true 就说明容器确实是被 OOM Killer 杀的。容器进程是被 SIGKILL 杀掉的,退出码是 137(128 加信号 9),所以看到 137 基本可以断定是内存问题。宿主机层面的 OOM 要看内核日志:
dmesg | grep -i oom
journalctl -k | grep -i oom日志里会出现类似「Out of memory: Killed process 12345 (mysqld)」的记录,并列出当时内存占用最大的几个进程。如果宿主机上根本没有其他业务,OOM 却频繁发生,通常有两种原因:一是容器限制设得比程序实际需求还小,进程还没启动完就被杀,这时适当调大 -m 并同步调整程序自身的缓冲参数;二是某个容器没限制、把内存吃满后波及全局,这时给所有容器补上限制即可。
另外要区分两种 OOM:容器内的 OOM 只影响容器本身,宿主机的 OOM 会随机挑选进程杀掉,甚至可能杀掉 sshd 让你彻底失联。给关键容器加上限制、并给宿主机预留 10% 到 20% 的内存余量,是避免「服务器突然失联」最有效的两个手段。
docker-compose 里的资源限制写法
用 docker-compose 编排服务时,资源限制写在服务定义里。老项目常见的写法是顶层键:
services:
web:
image: nginx:1.25
mem_limit: 512m
mem_reservation: 256m
cpus: 0.5
pids_limit: 128新版 docker compose(v2)更推荐把限制写进 deploy.resources 块,配合 docker compose up 直接使用也能生效:
services:
web:
image: nginx:1.25
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 128Mlimits 是硬上限,reservations 是预留量,含义和前面 docker run 的对应参数一致。不管用哪种写法,改完配置都要 docker compose up -d 重建容器,然后用 docker compose stats 确认限制真正生效——stats 里能看到每个容器的内存 LIMIT 列,如果显示为 0 或空,说明限制没写对。
实战:给一套博客站配置完整的资源方案
最后用一个完整例子收尾。假设一台 2G 内存、2 核的 VPS 上跑 Nginx、PHP-FPM 和 MySQL 三件套,合理的分配方案是:MySQL 限 768M(它最吃内存),PHP-FPM 限 512M,Nginx 限 256M,给系统和突发流量留出约 500M 余量。对应的 compose 配置可以这样写:
services:
nginx:
image: nginx:1.25
mem_limit: 256m
cpus: 0.5
pids_limit: 64
ports:
- '80:80'
- '443:443'
php:
image: php:8.3-fpm
mem_limit: 512m
cpus: 1.0
pids_limit: 256
db:
image: mysql:8.0
mem_limit: 768m
cpus: 1.0
command: --innodb-buffer-pool-size=384M配置完成后建议主动做一次压力验证:用 ab 或 wrk 并发请求网站,同时开另一个终端盯着 docker stats,观察内存有没有顶到上限、容器有没有被杀。如果 PHP 容器在压力下频繁 OOM,就把它的限制从 512M 调到 640M,同时检查 PHP-FPM 的 pm.max_children 是否设置过大——很多时候问题不是限制太小,而是进程池本身开得太宽。
运行中的容器如何调整限制
有些场景下容器已经跑起来了才发现限制不合理,比如流量涨了想给 PHP 容器多分点内存。不需要重建容器,docker update 可以直接修改运行中容器的内存和 CPU 限制,几秒钟生效:
docker update --memory 768m --cpus 1.0 php-app
# 也可以一次调整多个容器
docker update --memory 512m web-app db-appdocker update 支持调整 --memory、--memory-swap、--memory-reservation、--cpus、--cpu-shares、--cpuset-cpus 等参数,但不支持 pids-limit,进程数限制要改只能重建容器。调整之后用 docker inspect 确认是否生效:
docker inspect -f '内存上限: {{.HostConfig.Memory}} 字节' php-app
docker inspect -f 'CPU 上限: {{.HostConfig.NanoCpus}}' php-app需要注意的是,docker update 只能改容器创建时 Docker 允许动态调整的项,如果当初启动时没带任何限制参数,部分内核特性(比如 cgroup 相关配置)可能无法后补,最省事的办法还是养成「创建时就写好限制」的习惯。
最后回答两个新手经常问的问题。一是「为什么我限制了 512M,容器里的进程还是被杀?」——因为容器限制统计的是容器内所有进程的内存总和,PHP-FPM 的主进程加几十个 worker 进程、Nginx 的 worker 进程都要算进去,单看某一个进程的内存没有意义。排查时要对容器内全部进程的内存做加法,必要时调大限制或减少 worker 数量。二是「--cpus 和 --cpu-shares 能一起用吗?」——可以,两者并不冲突:--cpus 是硬上限,保证容器无论如何不会超过这个算力;--cpu-shares 只决定多个容器抢 CPU 时的优先级。个人站建议把 --cpus 当默认配置,--cpu-shares 留给少数需要保底的服务。
资源限制是 Docker 运维的基本功,也是个人站长花最少成本就能换来的稳定性保障。给每个容器都加上合理上限,再配好 OOM 排查手段,你的 VPS 就不会再因为一个失控容器而全线崩溃。记住三个要点:内存必须限、CPU 按需限、进程数顺手限。