程序手动运行正常,交给 systemd 却启动失败?工作目录、环境变量与权限排查

程序在 SSH 终端运行正常,交给 systemd 却启动失败,常见原因是目录、运行时、环境配置或服务权限不同。本文从退出状态和实际 unit 配置入手,逐项检查相对路径、虚拟环境、环境文件与安全隔离,并说明 drop-in 修改、配置重载、启动限制和回滚验收的方法,避免以扩大权限或反复重启代替排查。
程序手动运行正常,交给 systemd 却启动失败?工作目录、环境变量与权限排查

在 SSH 终端里进入项目目录,运行启动命令,程序正常。把同一条命令放进 systemd 服务,启动却失败:找不到配置文件、无法导入模块,或者提示 Permission denied。

这类故障通常要从运行上下文查起。终端里的当前目录、激活的虚拟环境、登录用户和 shell 配置,不会自动完整搬进系统服务。先找到失败发生在哪一步,再补齐具体条件,比反复重启更有效。

本文针对常见 Linux 发行版上的 systemd 系统级服务,命令中的 example.service、appsvc 和 /opt/example 都是示例。用户级服务需要使用对应用户的 systemctl --user,其默认目录和环境也有区别。以下按官方文档整理,未在你的生产服务器上执行;不同版本的字段支持应以本机手册为准。

一、把失败时间、退出状态和实际配置放在一起看

先做只读检查:

systemctl --version
sudo systemctl status example.service --no-pager -l
sudo journalctl -u example.service -b -n 100 --no-pager
sudo systemctl cat example.service
sudo systemctl show example.service \
  -p FragmentPath -p DropInPaths -p User -p Group \
  -p WorkingDirectory -p ExecStart -p EnvironmentFiles \
  -p Result -p ExecMainCode -p ExecMainStatus

journalctl -b 限定当前启动周期。如果故障发生在上一次开机或更早时间,需要调整查询范围。systemctl cat 展示主配置和 drop-in 文件,能发现“修改了一个文件,但另一份覆盖配置仍然生效”的情况。[1]

这些输出可能含命令参数、配置路径或内联环境变量。分享日志前应遮住密钥、令牌、连接串和用户信息。

有几类状态可以缩小范围:[2]

  • 200/CHDIR:切换工作目录失败,先检查路径和访问权限。
  • 203/EXEC:执行程序这一步失败,常见于可执行文件缺失、无法访问,或脚本解释器有问题。
  • 217/USER:用户凭据切换或用户命名空间设置失败,要结合 User= 和日志判断。
  • 普通退出码,例如 1:应用可能已经运行,随后因配置、依赖或其他业务错误退出,需要读应用日志。

这些编号是 systemd 定义的启动阶段错误,不应直接当成应用自己的业务退出码。仅凭 203/EXEC 也不能断定“程序没有执行权限”,缺失解释器和安全策略限制同样值得检查。

二、工作目录:相对路径最容易暴露环境差异

手动启动时,你可能先执行了:

cd /opt/example
/opt/example/.venv/bin/python /opt/example/app.py

应用随后读取 ./config.yaml。系统级服务未设置 WorkingDirectory= 时,默认工作目录是根目录 /,并不会根据脚本所在路径自动切换到项目目录。[2] 程序能找到 app.py,也不代表能找到相对路径的配置。

检查目录是否存在及整条路径的访问条件:

namei -l /opt/example
sudo -u appsvc test -x /opt/example
sudo -u appsvc test -r /opt/example/config.yaml

appsvc 必须替换成服务实际使用的用户。test 命令以退出状态反馈结果,不会输出成功消息;退出状态为零表示检查通过。路径上每一级目录都需要适当的搜索权限;配置文件本身还需要可读权限。

可以在 [Service] 中明确设置:

WorkingDirectory=/opt/example

如果程序支持独立指定配置路径,也可直接使用绝对路径。把工作目录和配置路径写清楚,部署时就少依赖“管理员碰巧在哪个目录执行命令”。

三、环境变量:虚拟环境和登录配置不会自动继承

在终端里 source .venv/bin/activate 后能运行,服务里报模块不存在,常见原因是用了另一套解释器。ExecStart= 使用虚拟环境解释器的绝对路径,可以避免依赖交互式激活:

ExecStart=/opt/example/.venv/bin/python /opt/example/app.py

Node.js 项目也要核对服务实际调用的运行时。只在某个登录用户的版本管理工具里配置的路径,系统服务可能用不到。不要把“SSH 中运行正常”当成运行时配置已完成的证据。

systemd 执行服务命令时不会默认启动登录 shell 来读取 .bashrc 或 .profile。需要的普通配置可用 Environment= 或 EnvironmentFile= 明确提供。[2]

Environment=APP_MODE=production
EnvironmentFile=/etc/example/example.env

示例环境文件内容:

APP_PORT=8080
APP_CONFIG=/opt/example/config.yaml

环境文件有自己的解析规则,不能直接视作 shell 脚本:不要依赖 export、命令替换或 $HOME 等 shell 展开。确需使用 shell 功能时,应显式调用选定的 shell 并审查参数,但无需仅为激活虚拟环境引入额外 shell。[2][3]

不要为了“先启动起来”就把必需环境文件写成带 - 前缀的可选路径。这样缺文件可能被忽略,问题转成更难定位的应用异常。

环境变量也不适合承载高敏感秘密。systemd 官方文档提示,服务环境可能通过管理接口暴露,并被后续进程继承。[2] 密钥优先使用应用支持的安全文件或本机 systemd 版本支持的凭据机制,按最小权限管理,避免把实际值放进文章、工单或日志。

四、ExecStart 的命令规则与终端有区别

终端中的命令可能含条件连接、输出重定向或管道,也可能依赖 alias、shell 函数或通配符展开。不能直接假定 systemd 会按终端 shell 的规则解释这些字符。[3]

排查时先把启动动作缩成一个明确的前台进程,使用可验证的可执行文件路径。对于脚本,还应检查首行解释器、文件格式和执行条件:

file /opt/example/start.sh
namei -l /opt/example/start.sh
findmnt -T /opt/example/start.sh -o TARGET,OPTIONS

脚本首行引用了不存在的解释器、文件带不合适的换行格式,或者所在挂载点有 noexec 限制,都可能影响执行。处理 noexec 或安全策略时,应先确认设置目的,不能为了启动应用就一律关闭限制。

程序已经以前台模式运行时,Type=simple 是常见配置。程序若自行 fork 到后台,则需要按产品文档调整运行模式或服务类型。也不要额外把程序放进 shell 后台,使它脱离预期的服务管理,否则进程跟踪和重启行为可能与预期不同。[3]

五、权限检查要覆盖写入目录与服务隔离

手工以 root 运行成功,服务以普通用户运行失败,并不稀奇。除了读取脚本和配置,还要检查应用写入的日志、缓存、上传和状态目录。

# 只检查权限,不创建测试文件
sudo -u appsvc test -w /var/lib/example
sudo -u appsvc test -w /var/log/example

目录不存在时,应按部署方案创建并配置准确的属主和权限;不要递归地授予所有用户读、写和执行权限。修改整棵目录的属主也可能破坏其他服务或安装包管理的文件。

普通文件权限通过了,服务仍然无法写入,还要查看 ProtectSystem=、ProtectHome=、ReadOnlyPaths=、ReadWritePaths=、RootDirectory= 等配置。[2] 服务可能看到一个受限制的文件系统视图。SELinux 或 AppArmor 的拒绝记录也应单独调查。

ReadWritePaths= 只能在相应隔离范围内放行路径,不能替代 Unix 权限,也不能把底层只读文件系统改成可写。需要持久状态目录时,可以评估 StateDirectory=;运行时目录可评估 RuntimeDirectory=,以本机手册和应用要求为准。

用 sudo -u appsvc 手工测试能发现部分用户权限问题,但无法完整重现 systemd 的环境、资源限制和命名空间。手工测试成功后,仍然需要回到真实服务中验收。

六、把确认过的条件写进配置,控制修改范围

下面是一个 Python 前台程序的字段组合示例。假定服务用户、项目、虚拟环境及环境文件已经存在;它不能直接替换任意现有服务配置:

[Unit]
Description=Example application

[Service]
Type=simple
User=appsvc
Group=appsvc
WorkingDirectory=/opt/example
EnvironmentFile=/etc/example/example.env
ExecStart=/opt/example/.venv/bin/python /opt/example/app.py
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

现有服务只应修改已经确认有问题的字段。操作前保存主 unit、相关 drop-in 和环境文件的备份,并记录文件属主、权限和原始内容;含敏感配置的备份也要限制访问。

可用 sudo systemctl edit example.service 建立 drop-in,例如只补上目录与环境文件:

[Service]
WorkingDirectory=/opt/example
EnvironmentFile=/etc/example/example.env

如果通过 drop-in 替换已有 ExecStart=,通常要先用空的 ExecStart= 清除旧定义,再写新命令;不要简单追加第二条启动命令。[3] 保存后用 systemctl cat 核对合并来源,防止误删已有依赖、安全设置或资源限制。

七、配置重载、服务启动和业务恢复分别验收

daemon-reload 让 systemd 重新读取 unit 配置,并不会自动把运行中的应用切换到新环境。reload 则是调用服务自身支持的重载动作,两者不能互换。[1]

在维护窗口完成配置检查后,使用真实 unit 文件路径进行验证,再重启目标服务:

# 以下路径只是示例;使用 FragmentPath 返回的实际路径
sudo systemd-analyze verify /etc/systemd/system/example.service
sudo systemctl daemon-reload
sudo systemctl restart example.service
sudo systemctl status example.service --no-pager -l
sudo journalctl -u example.service -b -n 100 --no-pager

systemd-analyze verify 能检查 unit 配置的部分错误,无法保证应用依赖、文件内容或真实请求正常。[5] restart 会停止并启动服务,单实例可能中断业务;需要事先安排流量、长任务和回退方案。

如果此前多次失败触发了启动速率限制,应先修好原因,再对目标服务执行:

sudo systemctl reset-failed example.service
sudo systemctl start example.service

reset-failed 清除失败状态及相关启动限制计数,不会修复启动条件。[1][4] 不要以连续重置和重启代替查日志。

验收包括服务状态稳定、近期日志无重复报错,以及真实接口、任务或写入功能正常。active 状态不能单独证明业务已就绪;还要观察是否反复重启。

如果新配置失败,恢复这次修改过的主配置或 drop-in 备份;若只新建了一个覆盖文件,就仅撤销该文件,保留原有 drop-in。然后重新执行 daemon-reload,在维护窗口恢复服务并验证。不要直接清空整个 .service.d 目录。

这类故障定位到最后,通常能对应到一项具体差异:服务在哪个目录运行、用哪套运行时、读取哪份配置、以谁的身份访问文件。把这些条件显式写进部署配置,下一次启动就不必依赖管理员的终端环境。

参考资料

资料核验日期:2026 年 10 月 10 日。当前在线文档可能包含新版本字段,旧发行版应同时查阅本机手册。

  1. systemd 官方手册:systemctl,配置查看、daemon-reload、restart 与 reset-failed。https://www.freedesktop.org/software/systemd/man/latest/systemctl.html
  2. systemd 官方手册:systemd.exec,工作目录、环境变量、服务用户、文件系统隔离及进程退出状态。https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
  3. systemd 官方手册:systemd.service,ExecStart、命令行解析与服务类型。https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
  4. systemd 官方手册:systemd.unit,StartLimitIntervalSec 与 StartLimitBurst。https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html
  5. systemd 官方手册:systemd-analyze,verify。https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html

在线手册相关内容同时通过 systemd 官方仓库中的对应手册源文件核对:https://github.com/systemd/systemd/tree/main/man

Linux运维

Linux 日志删了,磁盘空间为什么没释放?df、du 与已删除文件占用排查

2026-10-10 10:08:25

知识库

VPS、云服务器、独立服务器的区别是什么?新手服务器选择指南

2025-9-10 9:59:07