Laminar 1.4 配置参数详解
Laminar 1.4 配置体系逐项实测:服务端 7 个环境变量、job 级 TIMEOUT/DESCRIPTION/CONTEXTS、三层环境变量文件优先级、contexts 并发模型与隐式 default 消失陷阱、全局钩子与分组,附参数速查表与变更检查清单。
Laminar 1.4 配置参数详解
目标:把 Laminar 1.4 的配置体系拆成「服务端变量、job 级配置、contexts 并发模型、全局钩子」四层,每一层都给出真实服务器上的修改命令与生效验证。文中 11 组参数/变量均在 Ubuntu 24.04.5 + Laminar 1.4 上实测,回显照抄原始输出。
一、环境信息
| 项目 | 实测值 |
|---|---|
| 操作系统 | Ubuntu 24.04.5 LTS |
| 内核 | 7.0.0-38-generic |
| 主机名 | ci.example.com |
| Laminar 版本 | 1.4-1~upstream-ubuntu2404 |
| 服务端配置文件 | /etc/laminar.conf |
| 数据根目录 | /var/lib/laminar |
| 运行账户 | laminar(uid=995) |
| Web | *:8080 |
| RPC | 实测期间改为 unix:/var/lib/laminar/laminar.sock |
所有配置修改都通过 systemctl restart laminar 或热加载生效,验证手段统一为三种:curl 抓 HTTP/SSE 回显、laminarc 看队列、journalctl 看调度日志。
二、配置体系总览
Laminar 的配置分散在四个层次,改之前必须先判断改哪一层,否则会出现「改了没效果」或「重启把队列弄丢」:
| 层级 | 位置 | 生效方式 | 影响范围 |
|---|---|---|---|
| 服务端变量 | /etc/laminar.conf | systemctl restart laminar | 全局,含监听地址、标题、归档地址 |
| job 配置 | /var/lib/laminar/cfg/jobs/$JOB.conf | 热加载,下次调度读取 | 单个 job(超时、描述、上下文) |
| job 环境 | cfg/jobs/$JOB.env、cfg/env、cfg/contexts/$C.env | 下次 run 读取 | 单 job / 全局 / 单上下文 |
| 全局钩子 | cfg/before、cfg/after、cfg/scripts/、cfg/groups.conf | 热加载 | 所有 job |
热加载是真实存在的,新增 job 文件后日志里会立刻出现:
laminar.cpp:133: info: Reloading configuration
但 /etc/laminar.conf 不在热加载范围内——它是 systemd 的 EnvironmentFile,只读一次,必须重启进程。
三、服务端变量实测
3.1 LAMINAR_HOME
数据根目录,默认 /var/lib/laminar,配置模板里是注释状态:
#LAMINAR_HOME=/var/lib/laminar
它是所有路径的基准:cfg/(配置)、run/(运行目录与 workspace)、archive/(归档)、laminar.sqlite(历史库)都在它下面。修改它意味着同时迁移全部数据并保证 laminar 用户对新目录有读写权限,否则服务启动后无法创建 run 目录,job 会直接失败。没有特殊需求时保持默认。
3.2 LAMINAR_BIND_HTTP
控制 Web 监听地址,默认 *:8080,支持三种形式:
| 形式 | 示例 | 说明 |
|---|---|---|
| 通配地址 | *:8080 | 监听所有网卡 |
| 指定网卡 | 127.0.0.1:8136 | 只允许本机或反向代理访问 |
| unix socket | unix:/path/web.sock | 交给前置 Web 服务器 |
实测把它改成只监听本机 8136:
$ sudo sed -i 's|^LAMINAR_BIND_HTTP=.*|LAMINAR_BIND_HTTP=127.0.0.1:8136|' /etc/laminar.conf
$ sudo systemctl restart laminar
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8136/
200
$ curl -sS -o /dev/null -w "%{http_code}\n" --max-time 3 http://127.0.0.1:8080/
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to server
000
$ LAMINAR_HOST=unix:/var/lib/laminar/laminar.sock laminarc show-jobs
env-demo
fail
关键结论:HTTP 与 RPC 是两个独立端点。换掉 HTTP 端口不会影响 laminarc,因为客户端走的是 RPC;反过来改 RPC 也不影响浏览器。做端口收敛时两处要分别考虑。
3.3 LAMINAR_BIND_RPC
laminarc 连接守护进程的通道,默认 unix-abstract:laminar(抽象命名空间 socket,不落文件)。改成文件 socket 更便于用文件权限做访问控制:
$ sudo sed -i 's|^LAMINAR_BIND_RPC=.*|LAMINAR_BIND_RPC=unix:/var/lib/laminar/laminar.sock|' /etc/laminar.conf
$ sudo systemctl restart laminar
$ ls -l /var/lib/laminar/laminar.sock
srw-rw---- 1 laminar laminar 0 Oct 27 15:12 /var/lib/laminar/laminar.sock
srw-rw---- 表示只有 laminar 用户和 laminar 组能连接——这就是 Laminar 的「鉴权」:把运维账号加入 laminar 组即可,无需令牌。客户端通过环境变量指向 socket:
$ LAMINAR_HOST=unix:/var/lib/laminar/laminar.sock laminarc show-jobs
env-demo
fail
如果客户端仍按默认地址连接,会直接崩溃(这是 1.4 的真实行为,不是脚本问题):
$ laminarc show-jobs
exit=134
terminate called after throwing an instance of 'kj::ExceptionImpl'
what(): kj/async-io-unix.c++:1517: disconnected: connect(): Connection refused; address = unix-abstract:laminar
反过来,socket 路径选错会让守护进程起不来。实测把路径指向 /var/run/laminar.sock:
$ sudo sed -i 's|^LAMINAR_BIND_RPC=.*|LAMINAR_BIND_RPC=unix:/var/run/laminar.sock|' /etc/laminar.conf
$ sudo systemctl restart laminar
$ systemctl is-active laminar
failed
$ journalctl -u laminar --no-pager | tail -4
Oct 27 15:12:59 ci.example.com laminard[3880]: fatal: ::bind(sockfd, &addr.generic, addrlen): Permission denied; toString() = unix:/var/run/laminar.sock
Oct 27 15:12:59 ci.example.com laminard[3880]: laminard version 1.4-1~upstream-ubuntu2404 started
Oct 27 15:12:59 ci.example.com systemd[1]: laminar.service: Main process exited, code=exited, status=1/FAILURE
Oct 27 15:12:59 ci.example.com systemd[1]: laminar.service: Failed with result 'exit-code'.
原因很朴素:/run 归 root 所有,laminar 用户无权在其中创建 socket。要么把 socket 放在 /var/lib/laminar 下(推荐),要么预建 /run/laminar 并 chown laminar:laminar。
3.4 LAMINAR_TITLE
Web 页面标题,默认空。实测值:
LAMINAR_TITLE=Laminar CI - Build Farm
验证方式是抓 SSE 首帧(前端标题由 SSE 下发):
$ curl -s -N --max-time 2 -H "Accept: text/event-stream" http://127.0.0.1:8080/ | grep -o '"title":"[^"]*"'
"title":"Laminar CI - Build Farm"
这个标题同时出现在首页、Jobs 页和 Wallboard,是给大屏看板做标识最省事的一个变量。
3.5 LAMINAR_KEEP_RUNDIRS
默认 0,表示 run 目录(run/$JOB/$RUN,即脚本的工作目录)在构建结束后立即删除;设为 N 则保留最近 N 个。实测设为 2 后连续跑三次 inspect:
$ laminarc run inspect
inspect:8
$ laminarc run inspect
inspect:9
$ laminarc run inspect
inspect:10
$ ls -1 /var/lib/laminar/run/inspect
10
9
workspace
注意三点:
- 保留的是最新的 N 个,编号 8 的目录已被清理;
workspace不属于 rundir,永远保留,不受该变量影响;- 该变量与
archive/无关——归档从不清除,所以「保留现场」的正确做法是保留 rundir,而不是依赖 archive 里的产物。
对照默认值 0 的表现:跑完一次 first-job 后 run/first-job 下只剩 workspace,构建号目录已消失。排障时建议至少设成 1,否则失败现场的中间文件会一起被删掉。
3.6 LAMINAR_BASE_URL
当 Laminar 被反向代理挂在子路径(如 https://ci.example.com/ci/)时使用,影响前端 <base href>:
$ sudo sed -i '/^LAMINAR_BASE_URL=/d' /etc/laminar.conf
$ echo 'LAMINAR_BASE_URL=/ci/' | sudo tee -a /etc/laminar.conf
$ sudo systemctl restart laminar
$ curl -s --compressed http://127.0.0.1:8080/ | grep -o 'base href="[^"]*"'
base href="/ci/"
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/ci/js/app.js
404
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/js/app.js
200
这是一个必须理解清楚的半成品能力:它只改写 <base href>,不会真的把静态资源挂到 /ci/ 下。浏览器会先请求 /ci/(由反向代理转发),再按 base 标签请求 /ci/js/app.js——如果代理只做了前缀转发而不改写路径,就会 404。正确做法是在反向代理上把 /ci/ 前缀剥掉后再转给 Laminar 的根路径。
3.7 LAMINAR_ARCHIVE_URL
默认不设置,由 laminard 自己提供 /archive/...(官方手册明确指出这种方式「很朴素、效率低」,建议交给真正的 Web 服务器)。设置后,前端生成的产物链接会指向该地址:
LAMINAR_ARCHIVE_URL=http://ci.example.com/archive/
实测抓某次构建的 SSE 详情,产物链接已替换:
$ curl -s -N --max-time 2 -H "Accept: text/event-stream" http://127.0.0.1:8080/jobs/hello/8 | grep -o '"artifacts":\[[^]]*\]'
"artifacts":[{"url":"http://ci.example.com/archive/hello/8/build-time.txt","filename":"build-time.txt","size":21}]
该变量只影响链接前缀,不做任何上传或同步。你需要自己把 /var/lib/laminar/archive 通过 Nginx/Apache 暴露出来,并保证浏览器能访问到配置里的地址。
四、job 级配置
4.1 $JOB.conf:超时、描述、上下文
每个 job 可在 cfg/jobs/$JOB.conf 里声明三个键。实测文件:
TIMEOUT=5
DESCRIPTION=Demo job that exceeds its configured timeout
TIMEOUT 的单位是秒,超时后 Laminar 会终止整个进程组并把结果记为 aborted:
$ laminarc run timeout-demo
timeout-demo:2
$ echo $?
3
$ curl -s http://127.0.0.1:8080/log/timeout-demo/latest | tail -5
+ echo start
start
+ sleep 30
[laminar] Executing cfg/after
[global after] job=timeout-demo run=2 result=aborted last=aborted context=default
三个细节值得记住:
- 客户端退出码是 3,与普通失败(也是 3)相同,但结果字段是
aborted而不是failed; - 超时终止的是整个进程组,脚本里 fork 出去的子进程会一起被回收;
cfg/after仍会执行,并能通过$RESULT=aborted判断这是超时终止。
DESCRIPTION 会出现在 Job 详情页,实测 SSE:
$ curl -s -N --max-time 2 -H "Accept: text/event-stream" http://127.0.0.1:8080/jobs/timeout-demo | grep -o '"description":"[^"]*"'
"description":"Demo job that exceeds its configured timeout"
CONTEXTS 用于把 job 绑定到指定并发上下文(见第五节):
CONTEXTS=single
DESCRIPTION=Reads the per-context environment file
.conf 是热加载的,改完不需要重启,下一次调度即生效。
4.2 环境变量文件:三层来源与优先级
环境变量有三层文件来源,加载顺序固定:
cfg/env—— 全局,所有 job 可见;cfg/contexts/$CONTEXT.env—— 该上下文内的所有 job 可见;cfg/jobs/$JOB.env—— 仅该 job 可见。
后加载的会覆盖先加载的(内部用 overwrite=true 写入)。实测文件内容:
# cfg/env
GLOBAL_FLAG=on
# cfg/contexts/single.env
DUT_IP=192.168.0.123
# cfg/jobs/env-demo.env
QUOTED="a b"
PLAIN=value
用一个 job 打印它们,同时通过命令行传入同名参数:
$ laminarc run env-demo QUOTED=cli PLAIN=cli JOB_FLAG=off
env-demo:1
$ curl -s http://127.0.0.1:8080/log/env-demo/latest
+ echo 'QUOTED=["a b"]'
QUOTED=["a b"]
+ echo 'PLAIN=[value]'
PLAIN=[value]
+ echo 'GLOBAL_FLAG=[on]'
GLOBAL_FLAG=[on]
+ echo 'JOB_FLAG=[off]'
JOB_FLAG=[off]
从这四行能读出两条重要规则:
- 引号不会被剥离:
QUOTED="a b"的值是带引号的"a b",不是a b。这些文件不是 shell,不做展开,写什么就是什么; - 命令行参数不覆盖环境文件:
PLAIN=cli被PLAIN=value顶掉了。参数只用于填充「文件里没有定义」的变量,所以想让参数生效,就别在同名环境文件里再定义一次。
上下文环境文件的效果在另一个 job 上直观可见:
$ curl -s http://127.0.0.1:8080/log/single-env/latest
[laminar] Executing cfg/before
[global before] time=15:14:11 job=single-env
[laminar] Executing cfg/jobs/single-env.run
CONTEXT=single DUT_IP=192.168.0.123
[laminar] Executing cfg/after
[global after] job=single-env run=2 result=success last=success context=single
4.3 .init / .before / .after:执行时机
$JOB.init 只在 workspace 首次被创建时执行一次,适合放 git clone 或依赖初始化。实测建一个 init-demo:
# cfg/jobs/init-demo.init
#!/bin/bash -ex
echo "workspace created at $(date -u +%T)"
echo ready > "$WORKSPACE/state"
# cfg/jobs/init-demo.run
#!/bin/bash -ex
cat "$WORKSPACE/state"
第一次运行的日志:
$ curl -s http://127.0.0.1:8080/log/init-demo/1 | head -4
[laminar] Executing cfg/jobs/init-demo.init
++ date -u +%T
+ echo 'workspace created at 15:14:24'
workspace created at 15:14:24
第二次运行的日志里不再有 init,直接从 before 开始:
[laminar] Executing cfg/before
[global before] time=15:14:24 job=init-demo
[laminar] Executing cfg/jobs/init-demo.run
+ cat /var/lib/laminar/run/init-demo/workspace/state
ready
[laminar] Executing cfg/after
[global after] job=init-demo run=2 result=success last=success context=default
$JOB.before / $JOB.after 每次 run 都执行,顺序是:
init(仅首次) → cfg/before → $JOB.before → $JOB.run → $JOB.after → cfg/after
after 类脚本在失败或超时时也会执行,且能读到 $RESULT,适合做通知与清理。
五、Contexts 并发模型
5.1 语法
cfg/contexts/ 下每个文件就是一个上下文,文件名即上下文名:
# cfg/contexts/default.conf
EXECUTORS=4
JOBS=hello,inspect,fail,setdemo
# cfg/contexts/single.conf
EXECUTORS=1
JOBS=slow
EXECUTORS:该上下文允许同时运行几个 job;JOBS:用逗号分隔的 job 名列表(支持 glob),决定哪些 job 归属此上下文;- job 侧也可以用
$JOB.conf的CONTEXTS=反向声明归属。
5.2 单槽位排队实测
single 上下文只有 1 个执行槽,slow 每条要跑 60 秒。连续入队三次:
$ laminarc queue slow
slow:6
$ laminarc queue slow
slow:7
$ laminarc queue slow
slow:8
$ laminarc show-running
slow:6
$ laminarc show-queued
slow:7
slow:8
一个在跑、两个在等,符合 EXECUTORS=1。中止正在跑的那条:
$ laminarc abort slow 6
$ echo $?
0
$ laminarc show-running
slow:7
$ laminarc show-queued
slow:8
中止后队列立即推进:7 接替 6 开跑,8 继续排队。这说明 Laminar 的调度是「槽位释放即触发」而不是定时轮询。
5.3 隐式 default 上下文的消失(最容易踩的坑)
没有定义任何 context 文件时,Laminar 会隐式创建一个 6 槽位的 default,首次启动日志可见:
laminard[414]: laminar.cpp:560: info: Creating a default context with 6 executors
但只要 cfg/contexts/ 下出现任意 context 文件,这个隐式 default 就不再创建。实测把 default.conf 移走并重启后入队 hello:
$ mv /var/lib/laminar/cfg/contexts/default.conf /tmp/default.conf.off
$ sudo systemctl restart laminar
$ laminarc queue hello
hello:9
$ sleep 2
$ laminarc show-running
$ laminarc show-queued
hello:9
现象是:入队成功、永远不跑、日志没有任何报错。journal 里只有一行无害的入队记录:
laminard[3996]: rpc.cpp:55: info: RPC queue; jobName = hello
原因是未声明 CONTEXTS 的 job 期望归属 default,而 default 已经不存在,匹配不到任何上下文自然没有槽位。恢复 default.conf 并重启即可。
⚠️ 注意:这个坑的代价不止「排队不动」。Laminar 的队列在内存里,重启即丢。上面恢复配置后重启,排队中的
hello:9直接消失,show-running/show-queued均为空,log/hello/latest返回Not Found。所以排查此类问题时,先确认没有重要任务在排队再重启。
六、全局钩子、脚本目录与分组
6.1 cfg/before 与 cfg/after
两个文件对所有 job 生效,实测内容:
# cfg/before
#!/bin/bash
echo "[global before] time=$(date -u +%T) job=$JOB"
# cfg/after
#!/bin/bash
echo "[global after] job=$JOB run=$RUN result=$RESULT last=$LAST_RESULT context=$CONTEXT"
任意 job 的日志里都能看到它们:
[laminar] Executing cfg/before
[global before] time=15:15:02 job=env-demo
...
[laminar] Executing cfg/after
[global after] job=env-demo run=1 result=success last=unknown context=default
$LAST_RESULT 是该 job 上一次的结果,首次运行时为 unknown——这是判断「连续失败」的现成信号。超时中止时 after 仍会执行,$RESULT 为 aborted。
6.2 cfg/scripts:自动加入 PATH
cfg/scripts/ 目录会被前置到 PATH,里面的可执行脚本可以直接当命令调用。实测放一个:
# cfg/scripts/report-tool
#!/bin/bash
echo "report-tool invoked with: $*"
在 job 里直接调用:
+ report-tool alpha beta
report-tool invoked with: alpha beta
这一层适合放团队内部的构建辅助工具,避免在每个 job 里写长路径。
6.3 groups.conf:Web 端分组
cfg/groups.conf 用「组名=正则」的方式给 Jobs 页签分组:
Builds=^(hello|inspect|fail)$
All=.*
实测 SSE 返回的分组结果:
$ curl -s -N --max-time 2 -H "Accept: text/event-stream" http://127.0.0.1:8080/jobs | grep -o '"groups":{[^}]*}'
"groups":{"All":".*","Builds":"^(hello|inspect|fail)$"}
正则是对 job 名做匹配,.* 表示兜底分组。job 数量上到几十个之后,这个文件能显著改善看板可读性。
七、参数速查表
| 参数 / 文件 | 默认值 | 实测取值 | 生效方式 | 用途 |
|---|---|---|---|---|
LAMINAR_HOME | /var/lib/laminar | 默认 | 重启 | 数据根目录 |
LAMINAR_BIND_HTTP | *:8080 | 127.0.0.1:8136 | 重启 | Web 监听 |
LAMINAR_BIND_RPC | unix-abstract:laminar | unix:/var/lib/laminar/laminar.sock | 重启 | CLI 通道 |
LAMINAR_TITLE | 空 | Laminar CI - Build Farm | 重启 | 页面/大屏标题 |
LAMINAR_KEEP_RUNDIRS | 0 | 2 | 重启 | 保留最近 N 个 run 目录 |
LAMINAR_BASE_URL | 未设置 | /ci/ | 重启 | 子路径部署的 base 标签 |
LAMINAR_ARCHIVE_URL | 未设置 | http://ci.example.com/archive/ | 重启 | 产物链接前缀 |
$JOB.conf: TIMEOUT | 无限制 | 5 | 热加载 | 单 job 超时(结果 aborted) |
$JOB.conf: DESCRIPTION | 空 | 见上文 | 热加载 | Job 详情页描述 |
$JOB.conf: CONTEXTS | 空 | single | 热加载 | 绑定上下文 |
contexts/$C.conf: EXECUTORS | 6(仅隐式 default) | 4 / 1 | 热加载 | 并发槽位 |
contexts/$C.conf: JOBS | 空 | hello,inspect,... | 热加载 | 上下文归属 |
cfg/env、$JOB.env、contexts/$C.env | 无 | 见上文 | 下次 run | 环境变量三层来源 |
cfg/before、cfg/after | 无 | 见上文 | 热加载 | 全局前后置 |
cfg/scripts/ | 无 | report-tool | 热加载 | 加入 PATH 的工具 |
cfg/groups.conf | 无 | Builds=... | 热加载 | Web 分组 |
八、配置变更检查清单
改完配置后按这个顺序自查,能挡住绝大多数「改了不生效」:
- 改的是哪一层?
/etc/laminar.conf必须systemctl restart laminar;cfg/下的文件热加载。 - 重启前有没有排队任务? 队列在内存中,重启即丢,先
laminarc show-queued确认。 - RPC socket 路径的父目录是否可写? 不在
/var/lib/laminar下时先chown laminar:laminar,否则is-active直接failed。 - contexts 是否留下了没有归属的 job? 只要有 context 文件,就要显式提供
default.conf或给 job 写CONTEXTS=。 - 环境变量是不是被文件覆盖了? 命令行参数优先级最低,同名变量在 env 文件里存在时参数无效。
- 超时与失败是否分清? 超时结果字段是
aborted,退出码同样是 3。 - 改了
LAMINAR_BASE_URL之后反向代理是否正确剥前缀? 只改 base 标签,不移动静态资源。 LAMINAR_KEEP_RUNDIRS是否真的保留了现场? 只作用于run/$JOB/$RUN,不影响archive。
九、参考链接
- 官方站点:https://laminar.ohwg.net/
- 用户手册(配置与环境变量参考):https://laminar.ohwg.net/docs.html
- 源码仓库:https://github.com/ohwgiles/laminar
- 版本发布:https://github.com/ohwgiles/laminar/releases
系列导航:
- 《Laminar 1.4 完整部署与实战指南》 —— 定位、架构、能力边界与选型
- 《Laminar 1.4 安装与首个流水线》 —— 发行包与源码安装、目录语义、首个 job
- 《Laminar 1.4 常用 CLI 命令与运维实践》 ——
laminarc全命令族、运维集成与排障