DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

systemd 读取 EnvironmentFile 后,路径仍包含 ${BASE} 字面量

EnvironmentFile中的路径含变量名时,区分systemd与shell解析。静态值写完整,动态组合集中到受控启动脚本。

把 BASE=/opt/app 和 DATA=${BASE}/data 写进 EnvironmentFile 后,服务启动仍找不到目录,日志里的路径甚至带着 ${BASE}。如果这个文件由 systemd 读取,不能把它当作会执行变量赋值的 shell 脚本:变量值不会按前面的变量递归展开。

本文讨论 systemd 的 EnvironmentFile= 和服务启动命令。应用自己读取 .env 文件时是否展开变量,由应用的解析器决定;environment.d 也有独立规则,不能混为一谈。

先查看最终配置,而不是只在终端 source

以下是会产生字面量路径的示例:

BASE=/opt/app
DATA=${BASE}/data

终端执行 shell 的 source 或 . 可能得到 /opt/app/data,这只能说明 shell 解析了该文件,不能说明 systemd 也这样解析。先查看服务实际生效的 unit 与 drop-in:

systemctl cat app.service

app.service 是示例服务名。用户服务应使用 systemctl --user cat app.service,后续 reload、restart 命令也保持相同管理范围。检查 EnvironmentFile= 指向的文件,确认没有后来加载的文件覆盖同名变量;不要把包含密码的配置完整输出到共享日志。

静态路径直接写完整值

目录由部署配置决定时,把路径写完整,通常最容易核对:

BASE=/opt/app
DATA=/opt/app/data

文件中使用 KEY=value,不要写 export KEY=value,也不要在值中嵌入命令替换来生成路径。修改前保存原文件;若只修改环境文件的内容,systemd 会在下一次启动服务时读取,运行中的进程不会自动获得新值。若还修改了 unit 或 drop-in,则先执行对应范围的 daemon-reload。

重启会中断服务,确认窗口和回退配置后再进行。启动成功后检查应用自己的配置诊断或目录读写行为,避免用公开的环境变量转储作为验证办法。

确实需要运行时组合,放到受控启动脚本

需要根据 BASE 计算 DATA 时,可以保留环境文件中的完整 BASE,在自己的启动脚本里组合。下面是示例脚本,应用路径需替换为实际程序:

#!/bin/sh
: "${BASE:?BASE is required}"
DATA="$BASE/data"
export DATA
exec /opt/app/bin/server

unit 使用该脚本的绝对路径作为 ExecStart,脚本需要适当执行权限和正确所有者,避免服务用户能够修改由高权限服务执行的代码。exec 让应用接替启动脚本,保留服务管理中的主进程关系。修改启动方式前保存原 unit 和脚本,失败时恢复并重新加载配置。

不要为了任意层嵌套值使用 eval。路径和配置可能来自外部输入,重新解释为命令会引入额外执行风险。

为什么加花括号后结果又变了

如果 ExecStart 调用 sh -c,命令字符串可能先经过 systemd 的变量替换,再由 shell 解析。${DATA} 和嵌在字符串里的 $DATA 在 systemd 中并不具有完全相同的替换条件;于是某种写法看起来多展开一层,但这仍不是递归展开机制。

尽量让环境文件保存最终值,或在一处受控脚本计算。成功标准是服务实际使用完整目录、日志不再含有未展开的变量名,重启后结果稳定;只改变花括号并让一个示例输出正确,不足以证明任意嵌套配置可靠。