Docker Compose资源限制、健康检查与重启策略

3 分钟阅读·Matthieu·vpsproductioncontainersdocker-composedocker|

你的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设为512M1G这样的值来创建硬上限。将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-failurealways,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 filesMax 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-sizemax-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 团队原创作品。 未经书面许可,禁止复制、转载或再分发。

准备好亲自尝试了吗?

几秒内部署您自己的服务器。支持 Linux、Windows 或 FreeBSD。

查看 VPS 方案