OpenClaw Docker 最小可行部署:2GB 内存 + Compose 两条命令,避开 137 退出码

OpenClaw Docker 部署最小可行方案:2GB 内存、两条命令、一个退出码

结论先行:能跑,但要理解边界在哪

如果你只想拿结果:一台 2 核 2GB 的云服务器,装好 Docker 与 Compose 插件,把一份不到 40 行的 docker-compose.yml 放到 /opt/openclaw/,执行 docker compose up -d,再执行 docker compose logs -f 看到网关监听 18789 端口,OpenClaw Docker 部署就完成了。整套动作两条命令,第一次从零到能用,熟练的情况下十分钟以内。

但”能跑起来”和”跑得稳”之间,隔着一个著名的数字:137。这个退出码几乎出现在每一个新手群里,问法高度一致——”容器起来了,过了几分钟自己退了,日志最后一行是 exit code 137″。它不是一个 bug,它是 Linux 内核在内存不够时,从 OOM Killer 手里递给你的一张判决书。

这篇文章的目标不是让你背命令,而是让你理解这台机器上内存到底被谁吃掉了,以及在 2GB 这个刻度上,OpenClaw 的真实可用边界在哪里。我会给出可直接复制的 Compose 模板、可以照抄的排查链路、一张覆盖常见报错的对照表,也会坦白地讲清楚:什么时候你应当放弃 2GB,老老实实升到 4GB。

关于 OpenClaw 本身,先交代一句:它是开源的个人 AI Agent 网关,把模型调用、工具执行、会话状态收敛到一个服务里,对外暴露一个 Web 控制台和统一网关端口 18789。官方推荐用 Docker 部署,官方镜像在 ghcr.io/openclaw/openclaw。这些事实是本文全部配置的前提,后面的每一个参数都围绕它们展开。

为什么给 OpenClaw 选 Docker 部署,而不是直接装二进制

依赖收敛:一次构建,处处一致

OpenClaw 不是一个纯静态的 Web 服务,它要处理模型请求、执行工具调用、维护会话与状态文件。这类软件的直接安装方式往往牵扯到运行时版本、系统库、编译工具链,版本错一个,报错能淹死人。

Docker 部署最大的价值,是把”这台机器上装了什么”这件事从变量里删掉。镜像里是什么 Node 版本、什么系统库,全都固定住了,你在 Ubuntu 22.04 上跑出来的行为,和在 Debian 12、在群晖的 Container Manager 里跑出来的行为是一致的。出了问题,排查对象就只剩下你的配置和环境资源,而不是几千个软件包的可能性。

对个人 AI Agent 这类”要长期挂在自己机器上”的服务来说,这种确定性比性能更重要。你半年后回来看,镜像 tag 没变,配置文件没变,它就还能用;要是裸装,半年后系统升级一轮,很多东西就说不清了。

资源边界可控:这是避开 137 退出码的前提

这一点是本文的核心。OpenClaw Docker 部署之所以比裸装更容易避开 137 退出码,不是因为 Docker 更省内存,而是因为 Docker 允许你给容器划一条明确的内存红线,并且让超出这条红线的行为变成”容器内进程被杀”而不是”整台机器卡死”。

裸装的情况下,OpenClaw 的进程和系统其他东西共享全部内存,一旦它膨胀,OOM Killer 可能杀掉 sshd、可能杀掉你的数据库,甚至可能让整机失联。而在 Compose 里写一行 mem_limit: 1536m,游戏规则就变了:容器内内存触及红线,被处理掉的是容器自己,宿主机还活着,你还能 ssh 上去看日志、看 dmesg。

换句话说,Docker 给你的是”可控的失败”,而不是”不可控的雪崩”。在 2GB 这种紧巴巴的预算下,可控的失败比什么都重要。

升级与回滚:一条 pull 命令的事

Docker 部署的升级路径非常干净:改 tag,pull,up -d。回滚同样干净:把 tag 改回去,再 up -d。因为数据落在挂载卷上,不在容器里,所以容器换了,状态还在。后面会专门用一节讲清楚持久化和备份,这里先记住一个原则:容器是一次性的,数据不是。

什么时候 Docker 部署反而不是最优解

也要讲反面。如果你是在一台内存只有 1GB 的超低配 VPS 上折腾,Docker 守护进程本身、镜像层缓存、日志都会占掉一部分内存,这时候裸装反而更省。另外,如果你的机器上跑着其他同样吃内存的服务,Docker 的隔离并不能让总内存变多,只是让责任划分更清楚。

所以”Docker 部署”不是万能钥匙,它是”在 2GB 及以上、需要长期稳定运行、需要可回滚”这三个条件下最优的选择。本文的配置方案也是按这个前提设计的。

最小硬件配置核算:2GB 到底能撑住什么

先算一笔内存账:谁在吃你的 2GB

很多人对”2GB 内存”的理解是”OpenClaw 有 2GB 可用”,这是错的。一台 2GB 的云服务器,在 OpenClaw 容器启动之前,内存已经被分走了一大块。以下是我在实测环境中观察到的典型占用,仅供量级参考,具体数字随系统不同会有浮动:

  • Linux 内核与基础系统进程:约 150-250MB
  • Docker 守护进程 dockerd + containerd:约 100-150MB
  • sshd、systemd-journald、cron 等常驻服务:约 50-80MB
  • 文件系统缓存(可回收,但会挤压可用内存):动态,100MB 以上

这意味着,2GB 的机器上,真正能留给 OpenClaw 容器的,大致在 1.3GB 到 1.6GB 之间。这就是为什么后面 Compose 模板里我给的 mem_limit 是 1536m 而不是 2048m——把整个 2GB 都给容器,宿主机就会先被 OOM 打死,那种情况下容器未必被杀,但你的 ssh 会断,机器看起来”死了”,比 137 更难排查。

再说 CPU:2 核是下限,不是推荐

OpenClaw 本身的内存占用与并发强相关。CPU 核数决定的是”并发请求能被多快处理完”,而处理不完的请求会在内存里排队堆积,间接推高内存。2 核在个人使用场景(一两个会话、偶尔的工具调用)下够用;如果你是团队共享或者接入了高频的机器人回调,2 核会让队列变长,内存曲线随之陡起来。

这就是一个容易被忽略的因果链:CPU 不够 → 请求排队 → 内存堆积 → OOM → 137。所以只盯着内存调参数,有时候是治标不治本。

磁盘:镜像 + 数据 + 日志,留 10GB 比较安心

磁盘不是 137 的直接原因,但磁盘满会引发另一类典型故障,而且症状很像内存问题:容器起来了但写不进状态,日志刷屏,最后容器反复重启。建议起点:镜像与层缓存约 2-4GB,数据卷 2GB 以上,Docker 日志留出 1-2GB 余量。20GB 磁盘是比较舒服的配置,10GB 是硬下限。

2GB 与 4GB 的真实差别在哪里

官方的最低要求是 2GB RAM 能跑起来,正式生产部署建议 4GB。这两句话经常被读成”2GB 也能用”,但更准确的读法是:”2GB 是启动门槛,4GB 是稳定门槛。”

差别不在于能不能启动,而在于三件事:能否承受并发峰值、能否承受长时间运行的缓慢泄漏累积、能否在升级或重建容器的瞬间(新旧容器短暂共存、镜像解压)不被 OOM 波及。2GB 的机器在这三个瞬间是最脆弱的,而 137 往往就发生在这些瞬间。

四种配置方案横向对比:从 2GB 试验机到 4GB 生产机

在动手之前,先把配置和它的真实适用范围对齐。下面这张表是我建议的四档方案,你可以对照自己的用途直接选一档,不必从头试错。

方案 内存 / CPU 容器 mem_limit Swap 并发上限 适用场景 137 风险
A 最低可行 1GB / 1 核 不设(无意义) 1GB 1 纯验证安装流程,不建议长期运行 极高,随时可能被 OOM
B 本文推荐(2GB) 2GB / 2 核 1536m 2GB 2 个人自用、单人或 2 人共享、日活会话 < 20 中,需按本文调参
C 轻量团队(4GB) 4GB / 2 核 3072m 2GB 4 3-5 人共享、有定时任务回调 低
D 正式生产(4GB+) 4GB+ / 4 核 3072m-3584m 2-4GB 按压测定 对外服务、有 SLA 要求 低(前提是做了监控)

注意 A 档:1GB 内存我也见过不少教程说能跑。能启动是真的,但那种状态下的 OpenClaw 就像一个随时会断电的灯泡,你没法把它当成一个服务来对待。如果你手上真的只有 1GB,就把它当作”跑通流程、验证配置”的练习机,验证完就迁移,别在上面放真实数据。

Docker 与 Compose 环境准备:三条命令确认就绪

确认 Docker 与 Compose 插件版本

本文所有命令都基于 Docker Compose v2,也就是 docker compose(中间是空格)这种写法。老版本的 docker-compose(中间是横线)是 v1,已经停止维护,部分字段行为不一致,不建议使用。

docker --version
# Docker version 27.3.1, build ce12230

docker compose version
# Docker Compose version v2.29.7

free -h
#               total        used        free      shared  buff/cache   available
# Mem:           1.9Gi       210Mi       1.3Gi       1.0Mi       420Mi       1.6Gi

三个命令各看一个关键点:Docker 版本在 24 以上,Compose 是 v2,free -h 里 available 这一列就是你能真正拿到的内存。如果 available 不到 1.4Gi,先别急着部署,看看是不是有别的服务占了内存。

把 OpenClaw 放对位置:目录规划

不要图省事跑到 /root 下临时建目录。我建议固定一个位置,后面备份、升级、迁移都从这里出发:

  • /opt/openclaw/:放 docker-compose.yml 和 .env
  • /opt/openclaw/data/:挂载进容器,存配置与状态
  • /opt/openclaw/backup/:备份输出目录(可以指向外部挂载)

目录分清楚的好处,是备份的时候一句 tar 就能把数据卷完整打包,而不会把镜像缓存什么的也塞进去。

最小 docker-compose.yml 模板:逐行拆解

可复制模板

下面这份模板就是本文的核心,直接保存为 /opt/openclaw/docker-compose.yml。它包含了内存硬限制、日志轮转、健康检查、数据持久化这几件在 2GB 环境下必须做的事。

services:
  openclaw:
    image: ghcr.io/openclaw/openclaw:latest
    container_name: openclaw
    restart: unless-stopped
    ports:
      - "18789:18789"
    environment:
      - TZ=Asia/Shanghai
      - NODE_OPTIONS=--max-old-space-size=1024
      - OPENCLAW_MAX_CONCURRENCY=2
    volumes:
      - ./data:/app/data
    mem_limit: 1536m
    memswap_limit: 3584m
    cpus: "1.5"
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "3"
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:18789/ || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 60s

每一行为什么这么写

image 与 tag 的选择

镜像来自官方 ghcr.io/openclaw/openclaw。这里用 latest 是为了让第一次部署最省事,但如果你准备长期运行,请把它换成具体的版本 tag。原因很简单:latest 意味着”下次 pull 拉到的可能是一个语义完全不同的版本”,而升级应该是一件你主动决定的事,不该在重启时被动发生。

NODE_OPTIONS:给 Node 堆划一条线

OpenClaw 跑在 Node 上,而 Node 的旧生代堆默认上限会跟随系统内存自动设定。在容器里,这个自动推断经常会估得偏大,导致进程”以为”自己还有很多内存可以用,一直申请到越过 cgroup 红线,被内核直接干掉。137 退出码里,有很大一部分就是这个原因。

--max-old-space-size=1024 就是强制告诉 Node:堆最多用 1GB,超了你自己去 GC,别硬撑。这个值配合 1536m 的容器上限是经过权衡的:留大约 400-500MB 给堆外开销、文件描述符、网络缓冲。如果你把这两个数字设成一样的,比如都 1536,那 OOM 几乎是必然的。

mem_limit 与 memswap_limit:硬限制和软缓冲

mem_limit: 1536m 是硬限制,超出就会被杀。memswap_limit: 3584m 表示容器最多可以用 1536MB 物理内存加 2GB 的 swap。注意 swap 不会让程序变快,它只是把”立即死亡”换成”变慢但不死”,这一点后文专节展开。

顺带说一句:如果你用的是 Compose v2 较新的版本,mem_limit 这种写法在非 Swarm 模式下依然有效,但在某些发行版上是 deploy.resources.limits 的简写替代品。两者择一即可,不要混写,混写时后解析的字段会覆盖前面,可能得到和你预期不一样的结果。

日志轮转:2GB 机器上最容易被忽视的内存杀手

这一项我要单独强调。Docker 默认的 json-file 日志驱动不限制大小,默认情况下日志会一直长。在 2GB 的机器上,日志文件虽然写在磁盘,但 Docker 读取和写入日志时会占用页缓存,磁盘紧张时还会引发一系列连锁问题。更直接的后果是磁盘写满,容器写不进状态文件,看起来像”服务莫名其妙挂了”。

模板里 max-size: 20m + max-file: 3 意味着单个容器日志最多占 60MB,到顶自动轮转。这行配置我建议你无论什么配置都加上,成本为零。

healthcheck:让”假死”变成可见状态

OpenClaw 有一种典型故障:进程还在,但内部线程池卡住了,端口还能连,请求就是不返回。这时候 docker ps 里状态是 Up,但服务其实是死的。healthcheck 每隔 30 秒打一次本地 18789,连续失败 3 次,状态就会变成 unhealthy,你在 docker ps 里能一眼看出来,而不是等用户报障。

start_period: 60s 是给冷启动留的宽限期,避免容器刚起来就因为还没监听端口而被判定失败然后反复重启——那种重启循环在低配机器上非常常见,也很浪费内存。

两条命令完成 OpenClaw Docker 部署

第一条:拉取并后台启动

cd /opt/openclaw
docker compose up -d

# [+] Running 3/3
#  ✔ Network openclaw_default      Created
#  ✔ Container openclaw            Started

up -d 做了三件事:解析并校验 compose 文件、拉取镜像(首次会比较久)、创建网络并以分离模式启动容器。第一次执行时镜像下载可能需要几分钟,取决于你的网络到 ghcr.io 的连通性。

第二条:跟踪日志确认服务就绪

docker compose logs -f --tail 100 openclaw

看到网关开始监听 18789 端口,就说明服务起来了。按 Ctrl + C 退出日志跟踪,注意这不影响容器运行,只是退出查看。

如果这两条命令之后浏览器打不开 http://你的IP:18789,先别怀疑 OpenClaw,先查三件事:云厂商安全组是否放行了 18789、防火墙是否放行、容器是否处于 healthy 状态。docker compose ps 一条命令能同时看到状态和端口映射,是最省事的检查入口。

部署后前三十分钟,盯着这三项

我自己的习惯是:新部署的服务在第一个小时内至少看三次 docker stats,重点看 MEM USAGE 的曲线形状。三种形状对应三种结论:

  • 平稳在 600-900MB:很健康,2GB 够用,安心用
  • 缓慢爬升,每小时涨几十 MB:有内存泄漏倾向,需要关注,必要时设置定时重启
  • 几秒内冲到 1.5GB 然后容器消失:配置或负载有问题,直接按下一节的排查链路走

这条”看曲线”的动作,比任何参数调整都更有价值。参数是静态的,曲线会告诉你真实发生的事。

137 退出码的完整排查链路:从症状到根因

先确认:137 到底意味着什么

退出码 137 的算术是 128 + 9。9 是 SIGKILL 的信号编号,128 是 Unix 里”被信号终止”的固定偏移。所以 137 的准确含义是:进程收到 SIGKILL 而终止。SIGKILL 不能被进程捕获、不能被忽略,它只有几个来源,最常见的就是内核的 OOM Killer。

但也有一小部分 137 不是内存问题:比如你执行了 docker stop 而容器在 10 秒宽限期内没有优雅退出,Docker 会补一个 SIGKILL;或者手动 docker kill。所以排查的第一步不是加内存,而是先区分:是容器内进程自己越界被杀,还是有人在外面杀它。

第一步:看退出码与重启次数

docker inspect openclaw \
  --format '{{.State.ExitCode}} restarts={{.RestartCount}} oom={{.State.OOMKilled}}'

三个字段各有含义。ExitCode 为 137 是前提;OOMKilled 为 true 则几乎可以确定是内存问题;RestartCount 在短时间内迅速累加,说明它陷入了”启动—被杀—重启”的循环,循环本身会让内存状况更糟,因为崩溃前的内存还没完全回收,新进程又开始了。

第二步:用 dmesg 找到 OOM Killer 的行刑记录

这是最直接的证据。docker inspect 告诉你”被杀了”,dmesg 告诉你”谁杀的、杀了谁、当时内存多少”。

# 查看内核 OOM 记录(需要 root 或 sudo)
sudo dmesg -T | grep -i -E 'oom|killed process'

# 典型输出
# [Tue Nov  4 21:13:07 2026] openclaw invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0
# [Tue Nov  4 21:13:07 2026] memory: usage 1572864kB, limit 1572864kB, failcnt 142
# [Tue Nov  4 21:13:08 2026] Out of memory: Killed process 4128 (node) total-vm:1893240kB, anon-rss:1502344kB

读这段输出,重点看三个数:usage 和 limit 相等,说明撞的是你用 mem_limit 设定的那条线;anon-rss 是进程实际占用的物理内存;括号里的 (node) 说明被杀的是 Node 主进程。如果 limit 显示的是 1572864kB(也就是 1536MB),那就有力地证明容器限制起了作用,被杀的是容器内进程,而不是整机 OOM。

有些发行版默认不把 dmesg 输出给普通用户,如果提示权限不足,就加 sudo;如果是云厂商镜像把 dmesg 关掉了,可以看 /var/log/kern.log 或者 journalctl -k --since "1 hour ago" | grep -i oom,效果相同。

第三步:用 cgroup 的 memory.events 确认真实发生了什么

dmesg 是内核视角,有时会因为日志等级被过滤。更精确的是直接读容器的 cgroup 统计(cgroup v2 路径):

# 找到容器的完整 ID
CID=$(docker inspect -f '{{.Id}}' openclaw)

# 读取 OOM 计数(cgroup v2)
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.events

# 输出示例
# low 0
# high 0
# max 37        <-- 触及硬限制的次数
# oom 4         <-- 实际触发 OOM 的次数
# oom_kill 4    <-- 实际被杀进程数

max 大于 0 说明反复撞线,oom_kill 就是最终判决。这个文件是累计值,不会自动清零,所以看到非零不必惊慌,结合时间判断即可。如果你用的是 cgroup v1,路径是 /sys/fs/cgroup/memory/docker/<CID>/memory.oom_control。

第四步:排查内存到底被什么吃掉

知道是 OOM 之后,下一个问题是"内存去哪了"。用 docker stats 实时看,比看曲线更具体:

docker stats --no-stream openclaw
# CONTAINER   CPU %   MEM USAGE / LIMIT     MEM %   NET I/O
# openclaw    12.4%   1.42GiB / 1.5GiB     94.7%   1.2MB / 340kB

如果 MEM USAGE 一路贴着 LIMIT 走,基本可以断定是持续增长型的占用,而不是瞬时峰值。两者处理方式不同:瞬时峰值靠限并发、加 swap;持续增长则要怀疑配置、会话状态膨胀、或者某个工具调用在反复加载大文件。

第五步:判断是哪一类 OOM,对症下药

137 背后其实有四类不同的原因,处理方式完全不同。讲完前五步的取证方法,我把它们和证据、处置方式整理成一张对照表,这是本文最值得收藏的部分,也是后续四个调参杠杆的依据。

根因对照表:六种情况,六种处置

根因类别 典型现象 关键证据 处置方式 优先级
Node 堆上限过高 启动几分钟内稳定冲高后被杀 dmesg 中 node 进程 anon-rss 接近 mem_limit 设 NODE_OPTIONS=--max-old-space-size=1024,堆上限留出至少 30% 余量 高
并发过高 同时间有多个请求时被杀,空闲时正常 MEM 曲线呈锯齿状,高峰贴顶 降低 OPENCLAW_MAX_CONCURRENCY,2GB 建议 2,极端情况设 1 高
宿主机未预留内存 重启容器时整机 ssh 失联 dmesg 中出现 sshd、dockerd 被杀 mem_limit 调低到 1536m 以下,给宿主机留 300MB 以上 高
无 swap 导致的瞬时峰值 偶发被杀,重启后能稳定几小时 mem.events 中 oom_kill 偶尔 +1 加 2GB swap,并设 memswap_limit 允许容器使用 中
镜像层与数据同盘导致磁盘满 日志里有 ENOSPC,容器反复重启 df -h 接近 100% 清理旧镜像 docker image prune,迁移数据卷,开日志轮转 中
外部 docker kill / stop 超时 无 OOM 记录,但有 137 dmesg 无 oom 行,OOMKilled=false 加长 stop_grace_period,检查定时脚本是否在杀容器 低

这张表的使用顺序是从上往下:先确认是不是内存问题,再区分是堆、并发还是宿主机预留,最后才考虑换盘、换机。很多人的做法是直接升配到 4GB,问题"消失了"——其实是把问题从内存不足变成了内存充足,并没有解决为什么 2GB 会不够。如果你希望长期在 2GB 上跑,就得走完这条链路。

控制内存的四个杠杆:从最有效到最无效

杠杆一:限制内存(mem_limit)

这是最硬的杠杆。它不减少使用量,但把"爆炸半径"限制在容器内。这一步的意义是容错,不是优化。它让 137 从"整机灾难"变成"容器重启",而容器重启是有 restart: unless-stopped 兜底的。

但要注意:光靠 limit 并不能减少 137 的频次,它只是把死法从"全死"换成"局部死"。真正减少频次靠下面三个。

杠杆二:调低并发(最直接的降内存手段)

OpenClaw 的内存占用与并发关系近似线性。一次会话需要维持上下文、工具调用中间状态、请求缓冲,同时来三个,内存就是三倍。所以在 2GB 机器上,把并发压到 2 是本文配置的默认主张,如果你发现仍然在特定时段被杀,就压到 1。

压并发看起来是功能降级,实际上是把"一个用户被排队"换成了"两个用户同时崩溃"。从服务可用性的角度,前者永远优于后者。这一点在个人自用场景尤其成立——你不太可能同时发起五个复杂任务。

杠杆三:加 swap(把立即死亡换成变慢)

swap 在 2GB 机器上的正确理解是"缓冲垫",不是"扩容"。它让偶发的瞬时峰值有一个落脚点,而不是直接触发 OOM。做法分两步:宿主机上创建 swap 文件,Compose 里放开容器的 swap 额度。

# 1. 在宿主机创建 2GB swap 文件(只需一次)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# 2. 写入 fstab,重启后仍生效
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# 3. 降低换页倾向,避免频繁换入换出拖慢响应
sudo sysctl vm.swappiness=10
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf

# 4. 验证
free -h
swapon --show

关于 swappiness:默认值 60 意味着内核很愿意把内存页换出去。对 OpenClaw 这种交互式服务,把 swappiness 降到 10 更合适——优先用物理内存,只有真的不够时才换页。换出去的那部分一旦要回来,会造成明显的响应卡顿,用户能感知到。

有一个细节经常被忽略:如果 Compose 里设了 mem_limit 但 memswap_limit 也设成一样的值,那容器就完全用不到 swap,前面宿主机加的 swap 对容器毫无作用。所以 memswap_limit 必须严格大于 mem_limit,这是模板里写 3584m 的原因。

杠杆四:调低 Node 堆(最有效但最容易设错)

把 --max-old-space-size 调到 1024 甚至 896,是减少被杀频次最有效的单体动作。但这里有个反直觉的坑:设得太低,Node 会因为频繁 GC 而 CPU 飙高,请求处理变慢,排队变长,反而在别的环节出问题。

经验上,堆上限设为容器内存上限的 60%-70% 比较均衡。1536m 的容器配 1024 的堆,正好是 67%。如果你想压到 896,建议同时把并发再降一档,否则会出现"容器不死了但卡得要命"的新症状。

2GB 内存的真实可用边界:一个被低估的临界点

可用边界不在启动,在"重建瞬间"

这是我想单独讲的一个观察。绝大多数人评估 2GB 够不够,看的是稳态运行时的 docker stats。但 137 发生最多的时刻,不是稳态,而是三个"过渡瞬间":

  1. 升级时:pull 新镜像要解压层,磁盘 IO 和页缓存一起上升,旧容器还在跑,新容器同时启动,双份内存叠加。
  2. 重启时:进程退出到内存完全回收有延迟,接着新进程起来,中间有一个"双份内存"的窗口。
  3. 并发突发时:某个工具调用触发了大文件的加载或长链路的推理,单次会话的内存瞬时翻倍。

这三个瞬间的共同点是:内存需求是稳态的 1.5 到 2 倍,持续时间从几十秒到几分钟。稳态 700MB 的服务,在这几个窗口里会逼近 1.4GB。而你的 limit 是 1536MB——看起来够,但宿主机本身还要 400-500MB,真正的物理内存只有 2048MB。

所以我的判断是:2GB 的真实可用边界,不是"能不能跑",而是"能不能扛住一次升级"。如果你升级时经常被杀,说明这台机器的真实余量不足,该升 4GB 了。这也是一个很实用的自检:专门在低峰期做一次升级演练,看容器能否平稳过渡。

什么时候该升级到 4GB

给出几个明确的触发条件,满足任意一条就升级,不必纠结:

  • 每天出现一次以上 137,且在调低并发、加 swap 之后仍然没缓解
  • 用法从"个人自用"变成"两人以上共享",或接入了定时任务、Webhook 回调等高频繁触发
  • 需要在同一台机器上跑其他服务(数据库、监控、反向代理),宿主机内存被挤压
  • 有可用性要求,不能接受升级窗口期的服务中断

4GB 带来的不只是内存翻倍,而是"容器 limit 3072m + 宿主机 1GB 余量"这种健康的分配比例,上面说的三个过渡瞬间就都有了缓冲。这也是官方把 4GB 列为生产建议的原因,它不是营销数字,是对"升级瞬间"的预算。

一个反例:不升级也能稳的做法

也要讲清楚:不是所有 2GB 的 137 都要靠升级解决。如果你只是个人自用、每天晚上用一两个小时,那么完全可以通过错峰升级、定时重启、把并发压到 1 的方式,把 2GB 用出 4GB 的稳定度。代价是你放弃了一部分并发能力,以及需要多花点心思在日常维护上。这笔账划不划算,取决于你对"省下一台服务器"这件事的执念有多深。

数据持久化与备份:容器可弃,数据不可丢

挂载哪些目录

模板里只挂了一个目录:./data:/app/data。这个目录承载了 OpenClaw 的配置与运行状态,是唯一需要持久化的东西。原则是:凡是容器重建后会丢的,都应该在 data 里。镜像、临时文件、日志卷都不必挂载。

如果你不确定容器把状态写在哪,可以用一条命令看清楚:

docker inspect openclaw --format '{{json .Mounts}}' | python3 -m json.tool

它会列出所有挂载点和对应的宿主机路径,你就能确认数据是否真的落在了 /opt/openclaw/data,而不是容器内部的临时层。

备份:一个 tar 就够

在 2GB 的机器上,备份不需要任何复杂工具。停容器、打包、启容器,三步完成。建议放进 crontab 每天跑一次。

# 备份脚本 /opt/openclaw/backup.sh
#!/bin/bash
set -e
STAMP=$(date +%Y%m%d-%H%M)
cd /opt/openclaw
docker compose stop openclaw
tar -czf backup/openclaw-data-${STAMP}.tar.gz data/
docker compose start openclaw
# 只保留最近 7 份
ls -t backup/openclaw-data-*.tar.gz | tail -n +8 | xargs -r rm -f

为什么先停容器?因为 OpenClaw 运行时会写状态文件,边写边打包可能得到一份半截的文件,恢复时才发现坏了。停机通常只要几秒,代价很小。如果你的数据卷不大,这一步要不了半分钟。

恢复与迁移

恢复同样是三行:解压、确认权限、启动。迁移到新机器也是同理,新机器上装好 Docker,把 compose 文件和 tar 包拷过去解压,up -d 就完成了。这就是 Docker 部署在迁移上的最大优势:没有"环境依赖"的纠缠。

升级流程与回滚:把不可逆变成可逆

升级前三件事

  • 备份:先跑一次 backup.sh,确认备份文件大小合理(不是 0 字节)
  • 记录当前 tag:docker inspect openclaw --format '{{.Config.Image}}' 存下来,回滚时要用
  • 选时间:避开使用高峰,最好在你次日有空闲的时候做

升级两条命令

# 修改 docker-compose.yml 里的 image tag 到目标版本
docker compose pull
docker compose up -d

# 查看是否健康
docker compose ps
docker compose logs --tail 50 openclaw

注意顺序:先 pull 再 up。如果你直接 up,Docker 会自己拉,但拉取失败时它会沉默地继续用旧镜像,你会以为升级成功了。先 pull,报错会明确地暴露在网络层,你就能及时停下。

回滚:把 tag 改回去就行

回滚就是把 image tag 改回升级前的版本,再 up -d。这里有一个前提:升级后如果用了新版本的数据格式,旧版本可能读不了。所以要养成"升级后观察 24 小时再删备份"的习惯,万一需要回滚到旧版本,同时也要回滚数据卷。

这也是为什么我不推荐在生产环境用 latest:latest 让"我升级了什么"变成一个无法回答的问题,而回滚需要这个答案。

除 137 之外,OpenClaw Docker 部署还会踩哪些报错

下面这张表覆盖了 137 之外的常见故障。排查时先按"报错特征"定位,再看"含义",最后照"处理"执行。

报错 / 现象 含义 快速判断命令 处理方式
exit code 137 被 SIGKILL,通常为 OOM docker inspect … OOMKilled 见 137 排查链路,先降并发再限堆
端口被占用 bind: address already in use 18789 被其他进程或旧容器占用 ss -lntp | grep 18789 停掉占用进程,或改用 "18790:18789" 映射
permission denied while trying to connect to Docker daemon 当前用户不在 docker 组 id -nG sudo usermod -aG docker $USER 后重新登录
no space left on device 磁盘满,多为镜像层或日志堆积 df -h ; docker system df docker image prune -a;开日志轮转;迁数据卷
Error response from daemon: pull access denied ghcr.io 拉取权限或网络问题 docker pull ghcr.io/openclaw/openclaw:latest 检查网络出口、DNS,必要时配置代理
容器 Up 但页面打不开 端口未监听、防火墙、安全组或 healthcheck 失败 docker compose ps ; curl -I localhost:18789 逐层排查:容器内 → 宿主机 → 安全组
反复 Restarting 启动即崩,多为配置或数据卷权限 docker compose logs --tail 200 看第一行错误,确认 data 目录可写
unhealthy 健康检查连续失败 3 次 docker inspect --format '{{json .State.Health}}' openclaw 看失败是超时还是连接拒绝,区分卡死与未监听

这张表里最容易被误判的是"容器 Up 但页面打不开"。很多人直接重装,其实九成情况是安全组或防火墙,容器本身没问题。curl -I 127.0.0.1:18789 在宿主机上能通、外面不通,就锁定在防火墙这一层了。

适用场景与不适用场景

适合用这套 2GB 方案的情形

个人自用、单人主导的 AI 工作流、低频的工具调用、把 OpenClaw 当作"自己的助手网关"而不是"团队服务"。这些场景的共同特征是:并发低、可容忍偶发重启、不需要 SLA。在这种场景下,2GB 的 docker-compose 部署方案是性价比最高的选择,省下的钱可以花在模型额度上。

不适合的情形

团队共享、对外服务、有定时高频繁回调、需要与数据库同机部署、需要严格可用性承诺的场景,2GB 都不合适。前面那张配置对比表里的 C 档(4GB)是这类场景的起点,D 档是生产档。硬上 2GB 的代价,往往是每周花几个小时跟 137 搏斗,这些时间成本远超过升级服务器的费用。

一个中肯的建议

先按本文的 2GB 方案部署,跑一周。一周之内如果你一次 137 都没遇到、docker stats 的内存曲线平稳、升级演练顺利,那就说明你确实属于"2GB 够用"的人群,不必升级。如果一周内遇到两次以上,别犹豫,升 4GB。这个用一周数据说话的做法,比任何估算都靠谱。

常见问题解答(FAQ)

常见问题一:2GB 内存跑 OpenClaw Docker 部署,为什么一开始没事,用了几天才出现 137 退出码?

最常见的原因是内存增长型占用。可能有三种情况叠加:一是会话状态文件随时间累积,单次请求要加载的数据越来越多;二是 Node 的堆在达到 GC 阈值之前会持续增长,运行越久越接近上限;三是日志或临时文件没有轮转,间接挤压了可用内存。排查方法是用 docker stats 连续观察 24 小时的 MEM 曲线,如果是一条缓慢爬升的斜线而不是水平线,基本可以确认是增长型占用。对策是设置合理的内存限制加定时重启(例如每天凌晨 docker compose restart),同时开启日志轮转。如果曲线在几天内从 700MB 涨到 1.4GB,说明泄漏比较明显,这时候该考虑升级到 4GB,或者去查是哪个会话或工具调用导致的累积。

常见问题二:已经加了 swap,为什么容器还是会因为 OOM 被杀?

九成情况是 swap 配置了但容器用不到。Docker 容器的内存上限由两个参数共同决定:mem_limit 管物理内存,memswap_limit 管"物理内存 + swap"的总和。如果你只设了 mem_limit 而没设 memswap_limit,后者会默认等于前者,容器就完全用不到 swap。另外还要确认宿主机的 swap 确实已经启用,用 free -h 看 Swap 那一行的 total 是否非零,用 swapon --show 确认 swap 文件已激活。最后,vm.swappiness 过高会导致内核过早地把热数据换出,虽然不直接导致 OOM,但会拖慢响应、间接拉长请求处理时间,让内存堆积更久。建议 swappiness 设为 10。

常见问题三:docker-compose.yml 里的 mem_limit 和 deploy.resources.limits 应该用哪个?

在单机 Compose v2 环境下,两种写法都能生效,deploy.resources.limits.memory 会被 Compose 解析为等效的内存上限。mem_limit 是更直白的写法,阅读和维护成本低,推荐单机场景使用。deploy 段的写法原本是为 Swarm 模式设计的,如果你的配置需要在两种模式下兼容,才考虑用它。切记不要在同一份文件里同时写两处,因为两者的解析优先级在不同版本里不完全一致,混写可能得到一个你预期之外的值——最常见的结果是限制比你想象的要高,于是 OOM 依旧发生,而你看着配置文件以为是 1536m。升级 Compose 版本后,也建议用 docker compose config 把最终解析结果打印出来核对一遍。

常见问题四:为什么调整了 NODE_OPTIONS,Node 还是超过了堆上限被杀?

因为 --max-old-space-size 只管老生代堆,不管新生代、堆外内存、Buffer 和 native 模块。OpenClaw 在处理大文件、网络缓冲、或者加载某些 native 依赖时,会占用堆外内存,这部分不受该参数约束。所以正确的做法是让容器 mem_limit 明显高于 Node 堆上限,比如堆设 1024、容器设 1536,留出 30%-40% 的余量。如果你把两者设成同一个数字,比如都写 1536,那么堆外开销一上来就会撞线,表现为"我明明限制了堆,怎么还是 137"。另外,如果你把 NODE_OPTIONS 通过环境变量传入,注意某些启动脚本会覆盖它,可以用 docker exec openclaw env | grep NODE_OPTIONS 确认参数确实传进了容器。

常见问题五:升级 OpenClaw 版本时频繁出现 137,怎么平稳过渡?

升级时的 137 根因是"双份内存窗口":旧容器还在运行,新容器已经开始启动,两者短暂共存,总内存需求接近翻倍。在 2GB 机器上,这个窗口很容易越界。应对方式是先停后起,而不是让 up -d 自己滚动:先 docker compose stop openclaw 释放内存,再 docker compose pull 拉镜像(此时没有运行中的容器占内存),最后 docker compose up -d 启动新版本。这样虽然有几秒的中断,但在个人自用场景完全可接受,而且能大幅降低升级时被 OOM 的概率。如果你必须零中断,那就说明你的可用性要求已经超出 2GB 档位,应该升级到 4GB 并考虑真正的滚动发布方案。

常见问题六:Docker 部署的 OpenClaw 可以迁到另一台机器吗?数据会不会丢?

可以,而且这是 Docker 部署相比裸装最大的优势之一。迁移分三步:在旧机器上执行备份脚本,得到 data 目录的 tar 包;在新机器上装好 Docker 与 Compose 插件,创建 /opt/openclaw 并把 compose 文件和 tar 包拷过去;解压 tar 包到 ./data,执行 docker compose up -d。整个过程不涉及任何环境重建,因为运行时环境都在镜像里。需要注意两点:一是权限,解压后确认 data 目录对容器用户可写,否则容器会启动失败;二是版本一致,尽量让新机器先跑和旧机器相同的镜像 tag,确认服务正常后再单独做版本升级,把"迁移"和"升级"两个变量分开,出问题时才能定位。如果有防火墙或安全组,别忘了在新机器上放行 18789 端口。

THE END
喜欢就支持一下吧
点赞60 分享
评论 抢沙发
头像-知了AI
欢迎您留下宝贵的见解!
提交
头像-知了AI

昵称

取消
昵称表情代码快捷回复

    暂无评论内容