Skip to content

Commit 5ab6927

Browse files
committed
docs(systemd): 添加 systemd 服务配置要点和 npm/fnm 命令部署指南
添加了详细的 systemd 服务配置要点,包括 ExecStart 命令配置注意事项、npm/fnm 安装命令的部署方案(固定 PATH、动态执行 fnm env、TTY 兼容处理),以及验证服 务配置是否生效的方法。
1 parent e79fad1 commit 5ab6927

1 file changed

Lines changed: 152 additions & 0 deletions

File tree

docs/cheatsheet/linux/services/systemd.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,158 @@ WantedBy=multi-user.target # enable 时链接到该 target
161161
Alias=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

Comments
 (0)