你用 Docker Compose 起一个数据库容器,想在启动命令里加几个调优参数。参数是从一篇调优文章里抄的,看起来完全正确。可容器一起来就退出,日志里报的参数名,怎么看怎么眼熟——却又对不上。
现象
容器启动立即失败,日志里报类似:
unrecognized configuration parameter " shared_buffers"
FATAL: unrecognized configuration parameter " shared_buffers"
细看那句报错:参数名 shared_buffers 前面,多了一个空格。你回去看 compose 文件,自己写的明明是 shared_buffers=128MB,哪来的空格?
根因
问题出在列表的语义上。
在 Compose 的 command: 里,你可以写字符串形式,也可以写列表(YAML 数组)形式。当写成列表时,列表里的每一项,就是一个独立的参数,它们会被原样拼成 argv 传给进程。
假设你想传的是"选项 -c"加"它的值 shared_buffers=128MB"。正确的写法是两项:
command:
- -c
- shared_buffers=128MB
但很多人(包括写教程的人)会顺手把它们写进同一项:
command:
- -c shared_buffers=128MB # 错误:这是一项
这时候 Compose 会把它当成一个完整的参数传下去,于是进程收到的 argv 里只有一项:
argv[1] = "-c shared_buffers=128MB"
数据库解析命令行时,看到 -c,就认为"紧跟其后的那一段"是参数值,于是它把 -c 和后面的内容切开,取出 " shared_buffers=128MB" 作为要设置的配置项。注意这个字符串以空格开头——程序内部通常按 = 拆成"键"和"值",于是"键"就变成了 " shared_buffers"(带一个前导空格)。它拿着这个键去配置表里查,表里当然没有这一项,于是报"无法识别的配置参数"。
同样的坑不限于 -c。任何"选项 + 值"的结构(--flag value、-o file、--opt=val),只要塞进同一个列表项,都会变成"一个畸形参数"。反之,如果是 --opt=val 这种用等号连写的形式,本身就只需要一项,那倒是没问题——所以判断标准不是"看起来像几个词",而是"程序期望收到几个 argv 元素"。
一个快速自查的办法:让容器把最终命令行打印出来。
# 看容器真实的启动命令(以数组形式呈现)
docker inspect --format '{{json .Config.Cmd}}' <容器名>
如果输出里 -c shared_buffers=128MB 是一个被引号括起来的整体,那病因就坐实了。
解决
把选项和它的值拆成两项,各占一行:
services:
db:
image: postgres:16
command:
- -c
- shared_buffers=128MB
- -c
- work_mem=16MB
改完重建容器:
docker compose up -d
docker compose logs --tail=50 db
日志里不再出现 " shared_buffers",容器稳定运行,就对了。
如果你的参数特别多,还有更不容易出错的写法——直接用字符串形式,由 shell 去分词:
command: -c shared_buffers=128MB -c work_mem=16MB
不过要记住:字符串形式依赖 shell 分词规则,遇到值里含空格、引号、$ 的场合反而容易出错;列表形式虽然啰嗦,但每一项边界明确,是更稳的选择。
延伸与预防
这条坑值得记住的,是**"列表项的边界就是参数的边界"**这个语义。把"选项"和"它的值"当成两个概念,而不是"一句话"。
同类问题在别处也常见:写 docker run、写 Kubernetes 的 args、写 systemd 的 ExecStart(这里有自己的引号规则)、写各种 CLI 包装脚本时,都要分清"我是在提供一个参数,还是在提供两个"。判断方法很朴素——数一数:你期望进程收到几个参数,列表里就应该有几项。
最后一个实用习惯:容器起不来时,先看日志最上面那几行原始报错,别急着看自己写的配置。报错里那个"多出来的空格",往往就是整件事的线索。