systemd ExecStart 写了管道和 tee,日志文件却没有创建
ExecStart 直接写 | tee 不会启动 shell 管道。根据日志去向改用 journal 或有版本条件的 append 输出,核对用户服务范围、权限、覆盖文件和重启后的记录。
systemd 用户服务启动了 Node.js 应用,但 tee 指定的日志文件始终没有出现。如果单元里直接写了下面这行,问题首先在命令解析,而不一定在相对路径:
ExecStart=/usr/bin/node server.js | tee -a /home/appuser/logs/app.log
这里的 ExecStart= 没有显式调用 shell。systemd 不会把中间的 | 解释成管道,也不会因此启动 tee;这些内容会成为传给前面程序的参数。应用可能忽略这些参数,也可能因此报错,所以“服务还活着”不能证明日志管道已经运行。
下面以普通 systemctl --user 管理的服务为例,单位名为 demo.service。路径中的 appuser 是占位用户名,修改前换成实际用户与目录。系统级服务使用另一套管理范围,检查和修改时不要混用。
确认生效配置,而不只看编辑过的文件
systemctl --user cat demo.service
systemctl --user status demo.service
journalctl --user -u demo.service -n 50 --no-pager
cat 能显示单元文件和覆盖文件。检查最终使用的 ExecStart= 是否仍包含裸写的 | tee,以及有没有覆盖文件改了输出目标。日志若显示应用把 | 或 tee 当成非法参数,也符合这一解析方式。
如果原配置本来就是 /bin/sh -c '...',或者执行的是一个包含管道的脚本,就不属于“systemd 没有启动 shell”的情况,应继续检查脚本退出状态、日志目录和权限。不要仅因为没有文件就替换整个服务。
只需要查看日志时,直接交给 journal
应用已经向 stdout、stderr 写日志时,可以去掉管道,让 systemd 收集两个输出流。在原有服务中保留工作目录、环境变量及其他必要设置,将相关部分改为:
[Service]
WorkingDirectory=/home/appuser/app
ExecStart=/usr/bin/node /home/appuser/app/server.js
StandardOutput=journal
StandardError=journal
这是原文件中的配置片段。如果通过覆盖文件替换已有 ExecStart=,先用空赋值清除原命令,再设置新值:
[Service]
ExecStart=
ExecStart=/usr/bin/node /home/appuser/app/server.js
StandardOutput=journal
StandardError=journal
这种方式不会另外创建 app.log,需要用 journalctl 读取。它也不能捕获应用只写入自己内部文件、没有写到 stdout 或 stderr 的内容;那种情况应检查应用日志配置。
必须写文件时,使用 append 输出
systemd 240 起支持 StandardOutput=append:路径 和对应的错误输出设置。先用 systemctl --version 查看版本,并核对当前发行版的 systemd.exec 手册;旧版本或发行版回移功能可能不同。
日志目录应预先存在,并允许运行用户服务管理器的用户访问和写入。确认目标是预期的普通文件,避免写入共享目录中的不可信路径。然后在原服务中替换相关设置:
[Service]
ExecStart=/usr/bin/node /home/appuser/app/server.js
StandardOutput=append:/home/appuser/logs/app.log
StandardError=inherit
StandardError=inherit 在这里让 stderr 使用 stdout 的目标,因此两类输出都会进入同一个文件。通过覆盖文件改命令时,仍需先写一行空的 ExecStart=。不要为了创建日志目录给整个项目设置 777,也不要把用户服务改成 root 运行。
append: 会从文件末尾写入,不能用 file: 当作等价替代:后者从文件开头写入且不截断,可能覆盖已有内容的一部分。文件由服务管理器在启动程序前打开;系统级服务不能简单用应用的 User= 推断文件创建权限,仍需单独核对所有者和访问策略。
改为文件输出后,应用这两路日志不会同时自动复制到 journal。若必须同时保留 journal 和普通文件,应使用已有的日志采集/转发设施或应用自身的双输出能力。仅为复制日志加一层 shell 管道,会额外引入进程与退出状态管理,不是这个场景的首选。
加载修改并核对新日志
修改前备份单元文件及相关覆盖文件。重新读取配置不会自动重启应用;确认允许短暂中断后,再顺序执行:
systemctl --user daemon-reload
systemctl --user restart demo.service
systemctl --user status demo.service
如果出现配置解析警告或启动失败,先处理错误,不要把“命令没有输出”当作成功。选择 journal 时,用下面的命令观察应用正常操作产生的新消息:
journalctl --user -u demo.service -f
选择文件输出时,确认对应文件中出现的是重启后产生的新记录,且旧内容仍保留。文件存在只能证明创建成功,不能证明应用确实写了日志。
文件追加也不会自动解决轮转和容量控制。日志改名后,运行中的进程可能仍持有原文件描述符;应在维护窗口验证既有轮转方案如何重新打开输出,不能只创建一个新同名文件就认为切换完成。需要回退时恢复保存的配置,执行 daemon-reload,在允许中断时重启,并按恢复后的输出目标验证。保留已经写出的日志,不用清空文件来判断修复结果。


