Docker Compose资源限制、健康检查与重启策略
你的Compose文件在开发环境能跑,但还没有达到生产标准。学习如何添加内存/CPU限制、健康检查、重启策略和启动顺序,保护你的VPS免受OOM kill和级联故障的影响。
一个能正常执行docker compose up的Compose文件并不等于生产就绪。没有资源限制,一个容器就能吃光主机所有内存,触发Linux OOM killer,把VPS上的每个服务全部拖垮。没有健康检查,Docker无法检测到卡死的进程。没有重启策略,崩溃的容器会一直处于停止状态,直到你自己发现。
这三个系统协同工作:资源限制阻止失控的资源消耗,健康检查检测故障,重启策略负责恢复。本指南将三者作为Docker Compose v2的一体化生产加固层来讲解。
**前置条件:**VPS上有可用的Docker Compose环境。熟悉Compose文件的基本结构。如需复习,请参考。
如何在Docker Compose中设置内存和CPU限制?
在服务定义中使用deploy.resources.limits键。将memory设为512M或1G这样的值来创建硬上限。将cpus设为'0.5'这样的十进制字符串表示半个核心。容器进程超过内存限制时会被OOM kill。CPU限制会节流进程而不是杀掉它。
services:
api:
image: node:22-slim
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
效果:api容器最多可使用1个CPU核心和512 MB内存。Docker保证至少有0.25个核心和256 MB可供使用,即使其他容器在争抢资源。
验证限制是否生效:
docker compose up -d
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"
MEM USAGE / LIMIT列同时显示当前使用量和配置的上限。你应该看到512MiB作为限制,而不是主机的全部RAM。如果显示的是主机总内存,说明限制没有生效。
limits和reservations有什么区别?
Limits(限制)是硬上限。容器不能超过。Reservations(预留)是软保证。Docker用它们做调度决策和内存压力处理。
| 设置 | 作用 | 何时重要 |
|---|---|---|
limits.memory |
硬上限。超过即OOM kill。 | 始终。防止容器失控。 |
limits.cpus |
节流。进程变慢。 | CPU密集型负载(构建、推理)。 |
reservations.memory |
最低保证。 | 主机内存紧张时。 |
reservations.cpus |
CPU份额保证。 | 多个CPU密集型容器。 |
不设reservations的话,Docker按先到先得分配资源。压力之下,任何容器都可能被饿死。将reservations设为服务正常运行所需的最低值。
Docker容器超过内存限制时会发生什么?
Linux内核的OOM killer用SIGKILL终止容器的主进程。Docker记录退出码137(128 + 9,其中9是SIGKILL)。如果重启策略是on-failure或always,Docker会自动重启容器。
检查容器是否被OOM kill:
docker inspect api-1 --format '{{.State.OOMKilled}}'
输出:true确认发生了OOM kill。
查看更多细节:
docker inspect api-1 --format '{{json .State}}' | python3 -m json.tool
找到"OOMKilled": true、"ExitCode": 137和"RestartCount",了解重启了多少次。
没有限制时,容器会一直分配内存直到主机耗尽。内核随后在系统范围内杀死进程来释放RAM。这可能波及你的数据库、反向代理或SSH daemon。设置限制可以将爆炸半径控制在出问题的容器内。
如何在VPS上规划容器资源预算?
在资源固定的VPS上,你必须在所有容器之间分配内存。为主机操作系统和Docker本身留出余量。
8 GB VPS的预算示例:
| 组件 | 内存 |
|---|---|
| 主机OS + Docker引擎 | 1 GB |
| PostgreSQL | 2 GB |
| Redis | 512 MB |
| Node.js API | 1 GB |
| Nginx | 128 MB |
| 后台worker | 1 GB |
| 余量(未分配) | 2.35 GB |
保留20-30%不分配作为余量。如果容器限制的总和超过主机总RAM,就有可能触发主机级别的OOM killer,它会无视Docker的容器边界。
对比主机内存验证总分配:
free -h
docker stats --no-stream --format "{{.Name}}: {{.MemUsage}}"
如何在Docker Compose中配置健康检查?
在服务定义中添加healthcheck块。Docker按指定间隔运行测试命令,在达到配置的连续失败次数后将容器标记为unhealthy。其他服务可以依赖此健康状态来决定启动顺序。
services:
api:
image: node:22-slim
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
start_interval: 2s
健康检查参数说明:
| 参数 | 默认值 | 建议值 | 控制什么 |
|---|---|---|---|
interval |
30s | 15-60s | 启动后两次检查之间的时间 |
timeout |
30s | 5-10s | 单次检查的最大运行时间 |
retries |
3 | 3-5 | 标记为unhealthy前的失败次数 |
start_period |
0s | 10-60s | 慢启动服务的宽限期 |
start_interval |
5s | 2-5s | 启动期间的检查间隔(Compose v2.20+) |
start_interval参数(Compose v2.20新增)允许在启动期间更频繁地检查。容器在start_period内第一次检查通过时就从starting转为healthy。之后按正常interval运行检查。
健康检查中CMD和CMD-SHELL有什么区别?
CMD直接执行命令,不经过shell。CMD-SHELL通过/bin/sh -c运行。尽可能使用CMD。它避免了shell开销,也消除了PID 1问题——即shell而非你的检查命令接收信号。
# CMD格式 - 无shell,直接运行二进制文件
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
# CMD-SHELL格式 - 通过/bin/sh -c运行
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
# 字符串简写 - 等同于CMD-SHELL
healthcheck:
test: curl -f http://localhost:3000/health || exit 1
需要||、管道或变量展开等shell功能时使用CMD-SHELL。简单的二进制执行用CMD。
PostgreSQL、Redis和Nginx应该用什么健康检查命令?
每个服务都需要一个测试其处理请求能力的健康检查,而不是仅仅检查进程是否在运行。
| 服务 | 健康检查命令 | 测试内容 |
|---|---|---|
| PostgreSQL | ["CMD", "pg_isready", "-U", "postgres"] |
接受连接 |
| Redis | ["CMD", "redis-cli", "ping"] |
响应命令 |
| Nginx | `["CMD-SHELL", "curl -f http://localhost/ | |
| Node.js应用 | `["CMD-SHELL", "curl -f http://localhost:3000/health | |
| MySQL/MariaDB | ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] |
引擎就绪,不只是socket打开 |
**注意:**使用基于curl的健康检查时,镜像必须包含curl。很多slim镜像没有。在Dockerfile中安装它,改用wget,或编写一个你的应用框架原生提供的最小健康端点。
验证健康检查状态:
docker compose ps
在STATUS列中查找(healthy)或(unhealthy)。查看详细的健康检查历史:
docker inspect api-1 --format '{{json .State.Health}}' | python3 -m json.tool
这会显示最近几次检查结果,包括失败检查的stdout/stderr输出。注意观察:如果FailingStreak持续增加,说明你的检查命令有误或服务确实出了问题。
重启策略如何与健康检查交互?
重启策略控制容器退出时Docker的行为。健康检查控制Docker如何检测运行中容器的问题。二者结合形成自动恢复循环:健康检查检测到故障,容器被停止,重启策略将其拉起。
services:
api:
restart: on-failure:5
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
重启策略对比
| 策略 | 崩溃时 | 重启时 | docker stop时 |
适用场景 |
|---|---|---|---|---|
no |
保持停止 | 保持停止 | 保持停止 | 一次性任务、调试 |
always |
重启 | 重启 | 重启 | 核心基础设施(数据库、代理) |
unless-stopped |
重启 | 重启 | 保持停止 | 大多数生产服务 |
on-failure:N |
重启(最多N次) | 保持停止 | 保持停止 | 不应无限重启的服务 |
on-failure:5表示Docker最多重试5次。之后容器保持停止状态。这防止了在根本性故障的服务上浪费CPU的重启循环。
**OOM与重启的交互:**当容器达到内存限制并被OOM kill(退出码137)时,Docker将其视为失败。使用on-failure:5时,最多重启5次。如果服务存在内存泄漏,会反复被杀死和重启直到达到重试限制。检查重启次数:
docker inspect api-1 --format '{{.RestartCount}}'
大多数生产服务建议使用unless-stopped。它在崩溃和主机重启时自动重启,但尊重手动docker compose stop命令。对于崩溃循环应该触发告警而非静默重试的服务,使用on-failure:N。
如何让一个服务等待另一个服务健康就绪?
使用depends_on配合condition: service_healthy。这让Docker Compose等待依赖项的健康检查通过后再启动依赖服务。
services:
db:
image: postgres:17
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
restart: unless-stopped
api:
image: myapp:latest
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
restart: unless-stopped
不加condition: service_healthy的话,depends_on只等容器启动,不等里面的服务就绪。PostgreSQL需要几秒钟来初始化。你的应用会因为尝试连接尚未接受连接的数据库而崩溃。
depends_on中的restart: true选项(Compose v2.21+)告诉Docker在依赖项重启时也重启依赖服务:
depends_on:
db:
condition: service_healthy
restart: true
当你的应用缓存了数据库连接、无法在数据库重启后自行恢复时,这个选项很有用。
生产容器应设置哪些ulimits?
为处理大量并发连接的服务设置nofile(打开文件描述符数)和nproc(最大进程数)。每个TCP连接、打开的文件和管道都消耗一个文件描述符。很多镜像的默认限制(1024)对数据库和高流量服务来说太低了。
services:
db:
image: postgres:17
ulimits:
nofile:
soft: 65536
hard: 65536
nproc:
soft: 4096
hard: 4096
在容器内验证:
docker compose exec db cat /proc/1/limits
查找Max open files和Max processes。值应该与你设置的一致。
用PID限制防止fork bomb
设置deploy.resources.limits.pids来限制容器可以创建的进程数。这能防止fork bomb和失控的进程创建耗尽主机上所有PID。
services:
api:
image: myapp:latest
deploy:
resources:
limits:
pids: 200
如果你没有使用deploy.resources设置CPU/内存限制,顶层的pids_limit键也可以。但当deploy.resources.limits存在时,PID限制也必须放在那里。混用两种方式会在Compose v5+中导致验证错误。
200个PID对典型的Web应用来说足够宽裕。Node.js应用大约使用10-30个。PostgreSQL大约每个连接一个进程加上后台worker。按预期峰值的2-3倍来设置。
如何限制容器日志大小?
没有日志限制,容器日志会无限增长。一个输出量大的服务几小时就能填满你的磁盘。在json-file日志驱动上设置max-size和max-file来自动轮转日志。
services:
api:
image: myapp:latest
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
这最多保留3个10 MB的文件,每个服务的日志存储上限为30 MB。根据调试需求调整。 使用YAML锚点将相同的日志配置应用到所有服务:
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
services:
api:
image: myapp:latest
logging: *default-logging
worker:
image: myapp:latest
logging: *default-logging
设置stop_grace_period实现优雅关闭
Docker停止容器时,先发送SIGTERM并等待进程优雅退出。如果进程在宽限期内没有退出,Docker发送SIGKILL。默认值为10秒。
services:
db:
image: postgres:17
stop_grace_period: 30s
api:
image: myapp:latest
stop_grace_period: 5s
数据库需要更长的宽限期来刷写数据和干净地关闭连接。Web服务器和API进程通常几秒内就能退出。根据服务的实际关闭时间来设置宽限期,留一点缓冲。
完整的生产就绪Compose文件
这个示例组合了典型Web应用栈的所有设置:Nginx反向代理、Node.js API、PostgreSQL数据库和Redis缓存。
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
services:
nginx:
image: nginx:1.27-alpine
ports:
- "80:80"
- "443:443"
deploy:
resources:
limits:
cpus: '0.5'
memory: 128M
reservations:
memory: 64M
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
stop_grace_period: 5s
logging: *default-logging
depends_on:
api:
condition: service_healthy
api:
image: myapp:latest
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
pids: 200
reservations:
cpus: '0.25'
memory: 256M
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
start_interval: 2s
restart: unless-stopped
stop_grace_period: 5s
logging: *default-logging
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
deploy:
resources:
limits:
cpus: '1.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 1G
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
timeout: 3s
retries: 5
start_period: 15s
restart: unless-stopped
stop_grace_period: 30s
ulimits:
nofile:
soft: 65536
hard: 65536
logging: *default-logging
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
memory: 128M
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
stop_grace_period: 5s
logging: *default-logging
volumes:
pgdata:
secrets:
db_password:
file: ./secrets/db_password.txt
**注意:**数据库密码使用Docker secrets的POSTGRES_PASSWORD_FILE,而非明文的POSTGRES_PASSWORD环境变量。用受限权限创建secrets文件:
mkdir -p secrets
openssl rand -base64 32 > secrets/db_password.txt
chmod 600 secrets/db_password.txt
验证清单
应用所有设置后,逐项检查确认一切生效。
1. 检查资源限制是否生效:
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"
MEM USAGE / LIMIT列显示当前使用量和配置的上限。每个容器应显示其配置的内存限制,而非主机总RAM。
2. 检查健康检查状态:
docker compose ps
所有服务应在STATUS列显示(healthy)。如果有服务显示(health: starting),等待start_period结束。
3. 检查重启策略:
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}:{{.HostConfig.RestartPolicy.MaximumRetryCount}}' $(docker compose ps -q)
4. 检查容器内的ulimits:
docker compose exec db cat /proc/1/limits | grep -E "open files|processes"
5. 检查日志配置:
docker inspect --format '{{.HostConfig.LogConfig.Type}} max-size={{index .HostConfig.LogConfig.Config "max-size"}} max-file={{index .HostConfig.LogConfig.Config "max-file"}}' $(docker compose ps -q api)
6. 测试完整的恢复链:
停止一个容器并观察恢复过程:
docker compose stop api
docker compose ps # api应显示Exited
docker compose start api
docker compose ps # api应在start_period后显示(healthy)
docker inspect $(docker compose ps -q api) --format 'RestartCount: {{.RestartCount}}'
要测试真实崩溃下的自动重启,将内存限制降到应用基线以下。重启策略在进程自行退出时触发。注意,在较新版本的Docker中docker kill不会触发重启策略。
资源配置快速参考
VPS上常见服务的起始配置。根据实际负载调整。
| 服务 | 内存限制 | CPU限制 | 健康检查 |
|---|---|---|---|
| PostgreSQL | 1-4 GB | 1.0-2.0 | pg_isready -U postgres |
| Redis | 256M-1G | 0.25-0.5 | redis-cli ping |
| Node.js API | 256M-1G | 0.5-1.0 | curl -f http://localhost:PORT/health |
| Nginx | 64M-256M | 0.25-0.5 | curl -f http://localhost/ |
| Ollama (LLM) | 4-8 GB | 2.0-4.0 | curl -f http://localhost:11434/ |
| 后台worker | 256M-1G | 0.5-1.0 | 应用特定的检查 |
出了问题?
容器持续重启(重启循环):
docker compose logs api --tail 50
docker inspect api-1 --format '{{.State.ExitCode}} OOM:{{.State.OOMKilled}} Restarts:{{.RestartCount}}'
如果OOMKilled: true,增加内存限制。如果退出码不是137,检查应用日志找到实际错误。
健康检查始终失败:
docker inspect api-1 --format '{{json .State.Health.Log}}' | python3 -m json.tool
这会显示每次检查的输出。常见原因:健康端点不存在、镜像中未安装curl、或服务监听的端口与检查目标不同。
depends_on没有等待:
确保依赖项定义了healthcheck。没有它,condition: service_healthy无从等起,Compose启动时会报错。
docker stats中不显示限制:
确认你使用的是Docker Compose v2(docker compose插件,不是旧的docker-compose二进制文件)。检查版本:
docker compose version
deploy.resources键需要Compose v2。如果你使用的是旧版本,请参考获取安装说明。
出故障时查看日志:
journalctl -u docker -f
docker compose logs -f --tail 100
Docker daemon日志显示OOM事件和容器生命周期变化。Compose日志显示应用级别的输出。
版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。