上回说到,即使是 OpenClaw 官方文档,都不会给出配置文件的完整内容,OpenClaw 官方文档属于是在讲哪一部分就列出哪一部分的对应配置片段,所以,你会得到多个配置片段。因此,需要对这些配置片段进行合并,合并方式有两种:$include 指令和自定义合并脚本。虽然 OpenClaw 官方所推荐的 $include 指令只需要指定一下即可而自定义合并脚本是把分散的配置片段以复制的方式进行合并的,但是在面对类型检查、数组去重以及数组的逐元素合并等高阶合并操作方面,自定义合并脚本依旧不可替代!这一回我们继续来看一下另一个 OpenClaw 官方推荐的东西——Docker 沙箱容器——有哪些问题以及如何自定义 Docker 沙箱容器。当然,在开始之前首先需要声明的是自定义 Docker 沙箱容器绝不是在 OpenClaw 配置文件 openclaw.json 中的对应的位置指定一下这个 Docker 镜像的名称那么简单的。需要注意的以下的几个关键点:首先且最重要的是要让 Docker 沙箱容器长眠于后台而不是执行完一个命令之后就退出了;其次,这个基于 Docker 的沙箱容器执行命令会强制加上 sh -lc 前缀,其中,考虑到这个 sh -lc 的前缀,Docker 沙箱容器内的环境变量 PATH 可能会有一部分无法被加载;最后,我会说一下如何以正确的方式构建自定义的 Docker 沙箱容器。因此,我们首先来看一下为什么要让这个 Docker 沙箱容器长眠于后台外加上如何让 Docker 沙箱容器长眠于后台;其次,我们来看一下这个 sh -lc 前缀有哪些问题,其中需要重点关注的是为什么这个前缀会导致 Docker 沙箱容器内的环境变量会有一部分可能无法被加载;最后,我们来看一下如何以正确的方式构建自定义的 Docker 沙箱容器。首先我们来看一下为什么要让这个 Docker 沙箱容器长眠于后台,具体地说有以下两点原因:- 首先且最重要的是,如果这个 Docker 沙箱容器没有设置长眠于后台的操作,会导致 Docker 沙箱容器在执行完当前操作之后立刻退出,后续如果还需要执行其他操作 OpenClaw 说好听点就是会先启动 Docker 沙箱容器再执行对应操作,而且启动 Docker 沙箱容器和执行对应操作二者完全无法并行,因为启动 Docker 沙箱容器必须安排在执行的对应操作之前完成。换句话说就是你每次发号施令 OpenClaw 都需要先启动 Docker 沙箱容器再执行你所要求的对应的操作。
- 之前说好听点我们假设 OpenClaw 在发现 Docker 容器没有启动的时候会自行启动确保容错,可实际上说难听点 OpenClaw 非常有可能没有这样的容错机制,OpenClaw 非常有可能只会在创建 Docker 沙箱容器的过程中顺路完成启动操作,一旦 Docker 沙箱容器哪怕处于停止状态 OpenClaw 非常有可能不知道,以为还是处于运行状态。然而,我们的都知道把一个已经处于停止状态的 Docker 沙箱容器强行当做处于运行状态是有问题的,这个问题反映在 OpenClaw 上就是 OpenClaw 的会话会一直处于阻塞状态,会话显示回复不了一个字,一直转圈圈的那种状态。
需要注意的是 OpenClaw 到底有没有上述容错机制取决于具体版本,有的版本有这样的容错机制而有的版本没有这样的容错机制。因此,没有必要去赌这个容错机制 OpenClaw 到底有没有。毕竟,让 Docker 沙箱容器长眠于后台非常简单。首先,我们找到 openclaw-sandbox:bookworm-slim 默认 Docker 沙箱容器对应镜像的 Dockerfile,其对应内容如下所示:FROM debian:bookworm-slim@sha256:f9c6a2fd2ddbc23e336b6257a5245e31f996953ef06cd13a59fa0a1df2d5c252ENV DEBIAN_FRONTEND=noninteractiveRUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \ apt-get update \ && apt-get install -y --no-install-recommends \ bash \ ca-certificates \ curl \ git \ jq \ python3 \ ripgrepRUN useradd --create-home --shell /bin/bash sandboxUSER sandboxWORKDIR /home/sandboxCMD ["sleep", "infinity"]
让 Docker 沙箱容器长眠于后台的操作就是上述代码中的最后一行,这个理解起来非常简单,CMD 后续内容表示 Docker 沙箱容器启动时需要执行的操作,在这里上述代码让 Docker 沙箱容器启动时执行命令 sleep infinity,其中,这里的 sleep 表示让 Docker 沙箱容器处于等待状态,后面一个参数表示等待多长时间,infinity 表示等待时间为无穷大,即无限等待,永远等下去。如果说让 Docker 沙箱容器长眠于后台是自定义 OpenClaw 的 Docker 沙箱容器的关键点,那么这个被 OpenClaw 写死的 sh -lc 前缀就是重中之重,因为这个 sh -lc 前缀相比没有设置长眠于后台造成的问题更加隐蔽而且这个前缀的修改基本上就只能通过修改 OpenClaw 的源码来实现。因此,我们只能顺着 sh -lc 的意思来自定义 OpenClaw 的 Docker 沙箱容器。在此之前,我们需要简单地看一下 sh -lc 到底是个什么意思。其中,sh 是某一个 shell 脚本类型,具体是什么取决于对应的操作系统,比如说,它在红帽系列的操作系统中对应的是 POSIX 模式的 bash,在 Debian 系列的系统中对应的是 dash。-lc 表示可选参数,其需要拆成两个部分,即 -l 参数和 -c 参数,-l 表示这是一个登录 shell,其只会加载 /etc/profile 中的环境变量设置,.bashrc 诸如这样的位于用户 HOME 目录下的各种文件它非常有可能不会加载,所以当我们自定义 OpenClaw 的 Docker 沙箱容器的过程中千万不要以为 .bashrc 设置了环境变量就万事大吉了,至于如何设置环境变量后面会以环境变量 PATH 为例进行说明,这里我们先跳过。目前还差一个 -c 参数没有讲清楚,-c 的意思非常简单,后面跟一个字符串,该字符串就是执行的命令。比如说,我要求查看沙箱里面有没有我所期望的 Python 版本,在配置好 OpenClaw 之后假设给当前会话发送的消息是“执行 Linux 命令 python -V”,OpenClaw 就会执行这样的一个命令 sh -lc 'python -V'。接下来我们以环境变量 PATH 为例来说明如何给 OpenClaw 的 Docker 沙箱容器设置环境变量,总共有 5 种方式可以设置环境变量,但不是每一种方式都是正确的。Docker 沙箱容器的 /etc/profile我们首先来看一种正确的方式,即通过 Docker 沙箱容器的 /etc/profile 文件的执行逻辑设置环境变量,这种方式之所以正确是因为我就是这样做的,亲测有效的那种。在开始之前我们先看一下 Docker 沙箱容器的 /etc/profile 写了什么,对应内容如下图所示。需要注意点的是这个文件的内容不同的系统会有些微的区别,上述内容对应的系统是 WSL2 的 Ubuntu 26.04,Docker 沙箱容器内也有这个文件,就是内容可能会有些微的差别,但是逻辑基本上不变。因此这里仅以上述文件内容为例进行说明,从第 18 行到第 25 行这段代码可知,会依次加载 /etc/profile.d 目录下的所有以 .sh 结尾的文件,这里的加载顺序就是 /etc/profile.d 目录下的所有以 .sh 结尾的文件的文件名对应字符串的顺序,即先比较文件名对应字符串第一个字符的 ASCII 码值,获取 ASCII 码值小的那一个,如果两个文件名对应字符串的第一个字符是一样的,则继续比较的第二个字符的 ASCII 码值,获取获取 ASCII 码值小的那一个,以此类推。因此,小写字母 z 开头的 .sh 为后缀的文件会被最后加载。但是,如果有多个小写字母 z 开头的 .sh 为后缀的文件,它还会继续比较第二个字符,为了确保这个新增的设置环境变量的文件可被最后一个加载,可以先执行 ll -ah /etc/profile.d | grep \\.sh$ 命令列出该目录下的所有以 .sh 结尾的文件,看到最后一行对应文件名,如果不是以 z 开头则可以认为在该目录新建一个文件名字叫做 z.sh 会被 /etc/profile 最后一个加载。如果是以 z 开头,可以数一下开头有几个 z,假设开头有 k 个 z,那么只需要在 /etc/profile.d 目录下新建一个以 .sh 结尾的文件,点号前面是 k+1 个小写字母 z,没有其他字符。现在我们假设 /etc/profile.d 目录中没有以小写字母 z 开头且以 .sh 为后缀的文件名,即在该目录新建一个名字叫做 z.sh 的文件会被 /etc/profile 最后一个加载,到目前为止已经可以写出 Dockerfile 中注入环境变量 PATH 的代码。这个很简单,主要分两种情况讨论。首先我们来看第一种情况,如果要把自己的环境变量放在 PATH 最前面,则采用如下写法:RUN printf '\n' >> /etc/profile.d/z.shRUN echo 'export PATH=<自己的环境变量>:$PATH' >> /etc/profile.d/z.sh
需要注意的是这里只给出关键代码,至于像什么 FROM 指令啥的这里直接省略掉了。至于第二种情况就是把自己的环境变量放到 PATH 最后面,其中的关键代码如下所示:RUN printf '\n' >> /etc/profile.d/z.shRUN echo 'export PATH=$PATH:<自己的环境变量>' >> /etc/profile.d/z.sh
到目前为止第一个方法已经介绍完毕。考虑到这种方式取决于具体的系统或者环境,即不同系统中的 /etc/profile.d 目录中的文件会有所不同,所以适用范围相对而言非常受限。我用的镜像是 Rocky Linux 9 的系统,它不同于 Ubuntu 26.04,需要自己新建一个能在最后被 /etc/profile 加载的文件,它直接让最后一个被加载的文件名叫 sh.local。因此,/etc/profile.d/sh.local 就被我修改了,我在这个文件中的最后添加了如下一行:export PATH=$PATH:/home/sandbox/anaconda3/envs/python312/bin
这行代码就是把 Ananconda3 中的 Python 3.12 虚拟环境添加到 PATH 的最后。既然是 Anaconda3,那么必然有一个 base 环境,之后的例子我就以这个 base 环境为例,先记一下 base 环境的目录,如下所示:/home/sandbox/anaconda3/bin
因为我已经把一个 Python 3.12 的环境添加到了环境变量 PATH 的最后,再在环境变量 PATH 最后添加这个 base 环境就会一直无法访问,所以之后的测试一律令这个 base 环境放在最前面,如果最前面能成功则意味着该方法能成功。接下来我们看一下第二种方法,通过 Dockerfile 的 ENV 指令来实现,这个也比较简单,参考上面的内容依旧有两种情况,即把自己的的环境变量放到环境变量 PATH 的最前面还是最后面。如果是要把自己的环境变量放在 PATH 最前面,采用如下写法:FROM nvidia-cuda-12.1.1-cudnn8-devel-rockylinux9-imageENV PATH="/home/sandbox/anaconda3/bin:$PATH"
这里我直接给出了完整的 Dockerfile,其中 FROM 指令后面的内容是我自定义的一个 Docker 镜像,之前所谓的长眠于后台的操作也设置在其中,这里选择默认继承,只需要添加修改后的环境变量 PATH。接下来我们只需要通过它来构建一个镜像,执行以下命令:docker build --no-cache -t env-var-test .
接线来就是去 OpenClaw 的配置文件 openclaw.json 中的对应位置替换镜像名,具体是哪个字段哪个位置可以参考上上期的文章。一切准备就绪后就可以打开浏览器访问本地的 OpenClaw,登录完成之后直接新建一个会话,新建会话发送消息“执行 Linux 命令 python -V”,执行结果如图所示。需要注意的是在这里我能百分之百保证 python 命令一定走的是自己安装的这个 Anaconda3 下的环境而不是系统 python 是因为我知道在我的这个系统中的系统 python 不能通过简单的 python 来唤醒而要通过 python3 来唤醒。我们可以发现执行结果是 3.13 而不是 3.12,所以按照这种方式配置环境变量是能成功的。接下来我们继续看一下第三种方法,第三种方法比较简单,直接设置或者修改配置文件 openclaw.json 的 env.PATH 字段,这个方法为什么可能可行是因为其官方文档中的关键内容如图所示。注意到 PATH Handing 标题下的第二点,host=sandbox 的那一点。其中,第一句话说的是它会强制使用 sh -lc 前缀,这会导致环境变量 PATH 被重置,重点是后面一句话,后面一句话说的是 OpenClaw 的补救措施,即前置添加或者头部添加 env.PATH 字段值,可是这句话非常不可信,因为这句话在中文版官方文档提供的内容中并没有把 prepend 给翻译成前置添加或者头部添加,而是直接笼统的翻译成了添加,如图所示。因此,我们只能做个简单的测试。首先,我们需要去 OpenClaw 配置文件中添加或者修改 env.PATH 字段值,如下所示:{ ... "env": { "PATH": "/home/sandbox/anaconda3/bin" }}
需要注意的是,其余不相关的内容我这里直接使用省略号简写了。接下来就是和之前的步骤一样,打开浏览器访问本地的 OpenClaw,然后新建会话发消息。需要注意的是我们不再继续通过 Python 版本来判断是否添加成功,因为我们连是不是头部追加都不知道,尾部追加会导致 Python 3.12 会被先一步找到并使用,导致 Python 3.13 无法访问,即,误以为 Python 3.13 没有添加,从而导致误以为这个方法行不通。为了清楚地看到 Python 3.13 有没有添加,这里选择让它直接输出整个环境变量 PATH 中的所有 Python,那么发送的消息就要变成“执行 Linux 命令 type -a python”,结果如图所示。从结果中我们可以发现,这个方法是错的,因为只有一个 python 3.12,新添加的 python 3.13 并没有出现。配置文件的 tools.exec.pathPrepend 字段接下来我们看一下第四种方法,第四种方法和的第三种方法差不多,也是去修改配置文件 openclaw.json,只不过这次修改的是 tools.exec.pathPrepend 字段的值,如下所示:{ ... "tools": { ... "exec": { ... "pathPrepend": ["/home/sandbox/anaconda3/bin"] } }}
需要注意的是,其余不相关的内容我这里直接使用省略号简写了。接下来就是和之前的步骤一样,打开浏览器访问本地的 OpenClaw,然后新建会话发消息,消息和之前一样,直接让它执行命令 type -a python 即可,执行的结果如图所示。从结果中我们可以发现,这个方法是对的,因为两个版本的 Python 都能正确的找到,只不过需要注意的是这个字段的值的类型是个 JSON 数组。配置文件的 agents.defaults.sandbox.docker.env.PATH 字段接下来我们看一下第五种方法,这个方法依旧是修改配置文件中的某字段的值,只不过我们这次需要修改的是 OpenClaw 配置文件 openclaw.json 中的其名叫 agents.defaults.sandbox.docker.env.PATH 字段,如下所示:{ ... "agents": { ... "defaults": { ... "sandbox": { ... "docker": { ... "env": { ... "PATH": "/home/sandbox/anaconda3/bin" } } } } }}
需要注意的是,其余不相关的内容我这里直接使用省略号简写了。接下来就是和之前的步骤一样,打开浏览器访问本地的 OpenClaw,然后新建会话发消息,消息和之前一样,直接让它执行命令 type -a python 即可,执行的结果如图所示。这个方法依旧是正确的,因为两个版本的 Python 都能正确地找到。等一下,这不就意味着官方文档是错的吗?!其实,这么理解也没毛病,但我认为更恰当的解释是官方文档省略了 agents.defaults.sandbox.docker 这样的前缀。OpenClaw 的 Docker 沙箱容器设置环境变量 PATH 的 5 种方式 | | | | | |
|---|
| | | | | |
| | | | | |
| | | | | |
| 配置文件的 tools.exec.pathPrepend | | | | | |
| 配置文件的 agents.defaults.sandbox.docker.env.PATH | | | | | |
其中,追加方式为任意表示既可以头部追加也可以尾部追加。此外,需要注意的是配置文件 env.PATH 的对应行的后 4 列为空是因为该方法本身就不可行,讨论追加方式、是否需提供 PATH 位置、是否支持 Shell 展开外加是否只能设置环境变量 PATH 完全没有任何的意义。最后需要注意的是 OpenClaw 配置文件中的 agents.defaults.sandbox.env.PATH 字段虽然只能设置 PATH 环境变量,但是最后面的 PATH 完全可以改成其他的环境变量名,比如 LANG。尽管如此,但是如果改用其他的环境变量名,可能不会和 PATH 行为一致,环境变量 PATH 采用的是头部添加的方式,其他的可能是采用覆盖策略,也可能是采用尾部追加的策略。自定义 OpenClaw 的 Docker 沙箱容器的镜像最后我们看一下如何自定义 OpenClaw 的 Docker 沙箱容器的镜像,总共分为以下 6 个步骤:其中,<镜像名>和<摘要>根据实际情况换掉即可。如果不知道对应镜像的摘要可以去 Docker Hub 进行查询。docker run -it --name <容器名> <镜像名> bash -l
其中,<容器名>和<镜像名>根据实际情况换掉即可,-it 的意思是启用交互式环境,--name 是用于指定容器名的参数,bash 的意思是交互式环境所采用的终端是 bash,-l 参数表示该 shell 终端是一个登录 shell。此外,如无特别说明,以下操作都在容器内进行。第三,在临时容器内安装必要的系统软件或者工具并做好各种测试,同时可以根据登录 shell 必定会读取的 /etc/profile 文件的内容去 /etc/profile.d 目录中的某个文件(可以新建也可以用现成的,根据 /etc/profile 文件的执行逻辑来即可)设置必要的环境变量。当然,此时还可以进行一些其他操作,比如,创建一个非 root 用户、清理所有缓存文件、清理命令历史记录等等,只要容器不退出就行。第四,退出容器,这个最简单,一直执行 exit 命令直到容器退出,如果反复执行到整个终端都关掉了也没事,再开即可。此外,如无特别说明,以下操作都在容器外进行。docker commit \ --change 'USER <用户名>' \ # <用户名>根据实际情况修改,也可以去掉该行维持默认 --change 'WORKDIR <工作目录>' \ # <工作目录>根据实际情况修改,也可以去掉该行维持默认 --change 'CMD ["sleep", "infinity"]' \ # 设置长眠于后台的关键操作,不可去掉该行!!! <容器名> <镜像名>
其中,--change 就是我之前说的用来取代 Dockerfile 的,除了这里提到的指令 USER、指令 WORKDIR 外加上指令 CMD,其他的指令,比如 ENV 指令同样适用。第六,修改 OpenClaw 配置文件 openclaw.json 中关于 Docker 沙箱容器配置的相关字段把自定义的 Docker 镜像应用到其中,注意事项可以参考上上期的文章,这里不再进行详细的说明。在结束之前,首先我来简单地说明一下为什么不去编写 Dockerfile 来自定义镜像,主要原因就一个,如果软件之间的版本依赖安装时没事,但是运行时就会有事,这种情况 Dockerfile 很可能会生成一个有问题的 Docker 镜像,而我上述提供的不用 Dockerfile 的方式可以在打包镜像之前做好充分的测试。简单总结一下,首先,我简单地说了一下为什么要让 OpenClaw 的 Docker 沙箱容器长眠于后台,并且借助 OpenClaw 官方提供的 Docker 沙箱镜像的构建文件 Dockerfile 来找出长眠于后台的关键操作;其次,我详细说明了这个被写死的前缀 sh -lc 存在哪些问题,并且在其中详细地说明如何正确地设置环境变量;最后,我给出了自定义 OpenClaw 的 Docker 沙箱容器的镜像的步骤,只不过步骤中有些容易变化的东西只是简单地说明一下,其需要根据实际情况进行修改或者进行具体的操作。关于上述有不懂的可以后台回复“加群”,备注:Python 机器学习算法说书人,不备注可是会被拒绝的哦~!下一回,我们进入整个 OpenClaw 系列的最后一个内容,即自定义能访问英伟达 GPU 和 CUDA 并且能运行深度学习的代码的 OpenClaw 的 Docker 沙箱容器的镜像。