@@ -161,6 +161,158 @@ WantedBy=multi-user.target # enable 时链接到该 target
161161Alias =myapp.service # 别名
162162```
163163
164+ ### ⚠️ ExecStart / Command 配置要点
165+
166+ ` systemd ` 里的 ` ExecStart= ` 和你在交互 shell 里敲命令,不是同一个运行环境。
167+
168+ 最常见的坑有这些:
169+
170+ - ` systemd ` 默认** 不会读取** 你的 ` .bashrc ` 、` .zshrc ` 、` profile ` ,所以不要假设交互 shell 里的 ` PATH ` 一定存在。
171+ - ` ExecStart= ` 的** 第一个可执行程序** 最好是绝对路径,例如 ` /usr/bin/node ` 、` /usr/bin/env ` 、` /usr/bin/bash ` 。
172+ - 如果你想继续写裸命令,例如 ` zread ` 、` pm2 ` 、` pnpm ` ,建议写成 ` /usr/bin/env <command> ... ` ,并显式提供 ` PATH ` 。
173+ - 需要切目录时优先用 ` WorkingDirectory= ` ,不要把 ` cd /path && ... ` 全塞进 ` ExecStart= ` 。
174+ - 需要环境变量时,优先用 ` Environment= ` 或 ` EnvironmentFile= ` ,而不是把一大串 ` export ` 硬塞进命令。
175+ - 如果确实需要 shell 特性(如 ` && ` 、变量展开、` eval ` ),再用 ` /usr/bin/env bash -lc '...' ` 包一层。
176+ - 不是所有命令都适合做 ` Type=simple ` 常驻服务。像“打开浏览器”“交互选择版本”“依赖 TTY”的命令,在终端里能跑,不代表在 systemd 里会持续驻留。
177+
178+ 一个非常实用的判断标准:
179+
180+ - 命令在终端里启动后会** 长期前台占用** ,适合 ` Type=simple `
181+ - 命令启动后会** 立即退出** 、拉起子进程、弹浏览器、弹菜单,通常不适合作为常驻服务入口
182+
183+ ### 📦 npm / fnm 安装的命令如何部署为 systemd 服务
184+
185+ 很多 Node CLI 工具是通过 ` npm -g ` / ` pnpm add -g ` / ` fnm ` 安装的。
186+ 这类命令在终端里能直接运行,往往只是因为当前 shell 已经提前配置好了 ` PATH ` 。
187+
188+ systemd 下更推荐下面两种写法。
189+
190+ #### 方案 A:固定稳定 PATH(推荐)
191+
192+ 适合长期运行的 ` system service ` 。
193+
194+ 关键思路:
195+
196+ - ` ExecStart= ` 用 ` /usr/bin/env <command> `
197+ - ` Environment= ` 明确给出稳定的 ` PATH `
198+ - 不要使用 ` /run/user/.../fnm_multishells/... ` 这类会话级临时路径
199+
200+ 示例:
201+
202+ ``` ini
203+ [Service]
204+ Type =simple
205+ User =administrator
206+ Group =administrator
207+ WorkingDirectory =/home/administrator/projects/myapp
208+ Environment =" HOME=/home/administrator"
209+ Environment =" PATH=/home/administrator/.local/share/fnm/node-versions/v24.11.0/installation/bin:/usr/local/bin:/usr/bin:/bin"
210+ ExecStart =/usr/bin/env zread browse --host 0.0.0.0 --port 19681
211+ Restart =always
212+ RestartSec =3s
213+ ```
214+
215+ 这条线的优点是最稳定、最容易排障。
216+
217+ #### 方案 B:启动前动态执行 ` fnm env `
218+
219+ 适合你确实希望跟随 ` .node-version ` / ` .nvmrc ` 切换版本的场景。
220+
221+ 示例:
222+
223+ ``` ini
224+ [Service]
225+ Type =simple
226+ User =administrator
227+ Group =administrator
228+ WorkingDirectory =/home/administrator/projects/myapp
229+ Environment =" HOME=/home/administrator"
230+ ExecStart =/usr/bin/env bash -lc ' eval "$(/home/linuxbrew/.linuxbrew/bin/fnm env --shell bash)"; fnm use --silent-if-unchanged >/dev/null; exec zread browse --host 0.0.0.0 --port 19681'
231+ Restart =always
232+ RestartSec =3s
233+ ```
234+
235+ 这条线更灵活,但也更依赖 shell 包装和 ` fnm ` 本身的行为。
236+
237+ #### 方案 C:命令依赖 TTY 时,用 ` script ` 提供 PTY
238+
239+ 少数 CLI 命令在普通终端里能正常工作,但放进 systemd 这种** 无交互、无 TTY** 环境后,会出现:
240+
241+ - 进程启动后立即退出
242+ - ` systemctl start ` 返回成功,但端口没有监听
243+ - ` journalctl ` 里看到 ` status=0/SUCCESS ` ,同时 service 又不断 ` Restart=always `
244+
245+ 这类命令有时不是完全不能后台运行,而是** 需要一个伪终端(PTY)** 。
246+
247+ 此时可以用 ` /usr/bin/script ` 包一层:
248+
249+ ``` ini
250+ [Service]
251+ Type =simple
252+ User =administrator
253+ Group =administrator
254+ WorkingDirectory =/home/administrator/projects/myapp
255+ Environment =" HOME=/home/administrator"
256+ Environment =" PATH=/home/administrator/.local/share/fnm/node-versions/v24.11.0/installation/bin:/usr/local/bin:/usr/bin:/bin"
257+ Environment =" TERM=xterm-256color"
258+ Environment =" BROWSER=/bin/true"
259+ ExecStart =/usr/bin/script -q -c " zread browse --host 0.0.0.0 --port 19681" /dev/null
260+ Restart =always
261+ RestartSec =3s
262+ ```
263+
264+ 设计意图:
265+
266+ - ` /usr/bin/script ` :分配一个 PTY
267+ - ` -q ` :静默模式
268+ - ` -c "..." ` :执行真正的 CLI 命令
269+ - ` /dev/null ` :不额外保留 typescript 输出文件
270+ - ` TERM=xterm-256color ` :给依赖终端能力的程序一个基础终端类型
271+ - ` BROWSER=/bin/true ` :避免这类“浏览/打开”命令真的去弹浏览器
272+
273+ 这个方案是** 兼容手段** ,不是首选默认方案。
274+
275+ 推荐优先级仍然是:
276+
277+ 1 . 原生命令本身就支持稳定的 ` serve ` / ` server ` / ` daemon ` 模式
278+ 2 . 用稳定 ` PATH ` 或 ` fnm env ` 解决 Node / fnm 环境
279+ 3 . 只有确认问题根因是“缺少 TTY”时,才上 ` script -q -c '...' /dev/null `
280+
281+ #### 什么时候不该继续硬拗
282+
283+ 如果一个 npm CLI 的命令语义更像:
284+
285+ - 打开浏览器
286+ - 展示交互菜单
287+ - 做一次性生成 / 导出
288+ - 启动后立即退出,但后台可能短暂拉起别的东西
289+
290+ 那它可能并不适合作为 systemd 常驻服务入口。
291+
292+ 优先顺序建议是:
293+
294+ 1 . 看工具是否有明确的 ` serve ` / ` server ` / ` daemon ` 子命令
295+ 2 . 如果没有,再评估是否应该换成静态文件服务、反向代理,或其他真正适合常驻的服务方案
296+ 3 . 只有在充分验证后,才考虑用 ` script -q -c '...' /dev/null ` 这类 PTY 包装兼容手段
297+
298+ ### 🧪 验证 npm / fnm 服务配置是否真的生效
299+
300+ 不要只看 ` systemctl start ` 是否返回成功,至少还要一起检查:
301+
302+ ``` bash
303+ systemctl status myapp.service --no-pager -l
304+ journalctl -u myapp.service -n 100 --no-pager -l
305+ ss -lntp ' ( sport = :19681 )'
306+ curl -I http://127.0.0.1:19681
307+ ```
308+
309+ 常见信号:
310+
311+ - ` status=127 ` :通常是命令找不到,优先检查 ` PATH ` 和 ` ExecStart= ` 第一个程序
312+ - ` Active: activating (auto-restart) ` :通常是进程启动后又退出了,不能只看“start 成功”
313+ - ` curl: (7) Failed to connect ` :通常说明端口并没有真的监听
314+ - ` code=exited, status=0/SUCCESS ` 但服务不断重启:通常说明命令是“一次性命令”,不是适合常驻的服务入口
315+
164316### 🏷️ Service Type 类型
165317
166318| Type | 说明 |
0 commit comments