An environment file separates deployment values from the unit definition that describes process supervision. systemd reads the configured file before executing a service command and supplies those values in that process’s environment. Editing the file does not rewrite the environment of an already running process. A reload helper is another process, however, and can receive newer file values than the service’s existing main process.
When it breaks
daemon-reload tells the manager to reread unit definitions and rebuild its dependency information. reload requests the service’s configured reload operation, such as ExecReload. Neither command is a universal instruction to replace an existing process environment. A service may implement application-specific configuration reload, but that behavior must come from the application or its reload helper.
An EnvironmentFile path prefixed with a minus sign is optional. If it is missing, systemd ignores that absence without the warning an operator might expect. A typo can therefore silently remove an override. Use optional files only when their absence has a valid meaning; a required deployment input should not disappear behind that prefix.
The file is parsed by systemd, not sourced by a shell. Quoting and escape rules exist, but command substitution and variable expansion are not shell execution. A value containing $HOME remains that text. Files listed later override earlier values, and file assignments override Environment assignments. Inspect precedence rather than assuming the first visible definition wins.
Check it quickly
Run this Python script as an administrator in an isolated Linux environment with systemd as PID 1. It uses only harmless fixture settings and temporary paths. The recorded run used Debian 12 with systemd 252.39. Ensure the example runtime unit name is unused. The worker records only four selected test values, never a general environment dump:
from pathlib import Path
from tempfile import TemporaryDirectory
import json,subprocess,time
unit='xbcatm-environment-lab.service';unit_path=Path('/run/systemd/system')/unit
assert not unit_path.exists()
def ctl(*args,success=True):
result=subprocess.run(['systemctl',*args],capture_output=True,text=True,timeout=10)
assert (result.returncode==0)==success,(args,result.stderr)
return result.stdout.strip()
def value(key):return ctl('show',unit,'-p',key,'--value')
def wait_for(check):
deadline=time.monotonic()+5
while not check():
assert time.monotonic()<deadline,'state transition timed out'
time.sleep(0.02)
with TemporaryDirectory(prefix='xbcatm-environment-',dir='/run') as directory:
root=Path(directory);first=root/'required.env';second=root/'optional.env';worker=root/'worker.py'
command=f'$(touch {root}/unexpected)'
initial=f'MODE=first\nLITERAL=$HOME\nQUOTED="two words"\nCOMMAND={command}\n'
first.write_text(initial);first.chmod(0o600);second.write_text('MODE=second\n')
worker.write_text("""from pathlib import Path
import json,os,sys,time
root=Path(__file__).parent;name=sys.argv[1]
data={key:os.environ.get(key) for key in ('MODE','LITERAL','QUOTED','COMMAND')}
data['pid']=os.getpid()
tmp=root/(name+'.tmp');tmp.write_text(json.dumps(data));tmp.replace(root/(name+'.json'))
if name=='main': time.sleep(60)
""")
unit_path.write_text(f'''[Service]
Type=exec
Environment=MODE=unit-default
EnvironmentFile={first}
EnvironmentFile=-{second}
ExecStart=/usr/bin/python3 {worker} main
ExecReload=/usr/bin/python3 {worker} reload
''')
def read(name):return json.loads((root/(name+'.json')).read_text())
try:
subprocess.run(['systemd-analyze','verify',str(unit_path)],check=True)
ctl('daemon-reload');ctl('start',unit)
wait_for(lambda:(root/'main.json').exists())
current=read('main');assert current['MODE']=='second'
assert current['LITERAL']=='$HOME' and current['QUOTED']=='two words'
assert current['COMMAND']==command and not (root/'unexpected').exists()
assert 'MODE=unit-default' in value('Environment')
print('ordered environment files override unit value; quoting works; shell text stays literal')
second.write_text('MODE=third\n')
ctl('daemon-reload');assert value('MainPID')==str(current['pid'])
live=Path(f'/proc/{current["pid"]}/environ').read_bytes().split(b'\0')
assert b'MODE=second' in live
ctl('reload',unit)
assert read('reload')['MODE']=='third' and read('main')==current
assert value('MainPID')==str(current['pid'])
live=Path(f'/proc/{current["pid"]}/environ').read_bytes().split(b'\0')
assert b'MODE=second' in live
print('reload helper receives new file value; existing main process retains old environment')
second.write_text('MODE=fourth\n');ctl('restart',unit)
wait_for(lambda:read('main')['pid']!=current['pid'])
assert read('main')['MODE']=='fourth'
print('restart reads changed environment file without another daemon-reload')
first.unlink()
subprocess.run(['systemd-analyze','verify',str(unit_path)],check=True)
ctl('restart',unit,success=False)
assert value('ActiveState')=='failed' and value('MainPID')=='0'
print('missing required environment file prevents start')
first.write_text(initial);first.chmod(0o600);second.unlink()
ctl('reset-failed',unit);ctl('start',unit)
wait_for(lambda:read('main')['MODE']=='first')
print('missing optional file is ignored; required-file value remains')
finally:
ctl('stop',unit)
if value('ActiveState')=='failed':ctl('reset-failed',unit)
unit_path.unlink();ctl('daemon-reload')
The main process initially receives MODE=second: the optional second file overrides the required first file, which overrides the unit’s default. The quoted value becomes two words with a space. The variable-looking and command-looking values stay literal, and the file that a shell substitution would have created does not exist.
The manager’s Environment property still contains the unit’s default assignment. It is not a merged report of every value read from environment files. The worker’s selected observations provide the effective values for this fixture; the property identifies configured unit assignments. Confusing those views can lead an operator to diagnose the wrong setting.
After editing the file to a third value, daemon-reload leaves the main PID and its actual /proc environment unchanged. Running the reload helper gives that new helper the third value, while the original main process still contains the second. The code checks the specific harmless entry without printing the rest of the process environment.
The file is edited again to a fourth value, then the service is restarted without another manager reload. The replacement process receives the fourth value. This isolates fresh file reading at process execution from reloading unit definitions. It also shows why a successful reload helper does not alone prove that the main application adopted a replacement setting.
Removing the required file still permits static unit verification but makes the actual restart fail with no main PID. Restoring that file and removing the optional one allows startup with the first file’s value. Static validation, successful file loading, and application adoption are three separate checks.
Keep configuration visibility selective
Use environment files for suitable configuration, with ownership and permissions appropriate to the service. Sensitive values still become process environment values and may be inherited or exposed through diagnostics. The filename being outside version control does not establish a complete secret boundary. Use the platform’s supported credential-delivery mechanism when that better matches the secret’s requirements.
Document whether a change requires a new process, a supported application reload, or both. Change unit definitions through the appropriate override location and reload the manager when those definitions change. Editing only an existing environment file does not require that reload, but the intended consuming process must actually execute or otherwise implement an explicit refresh path.
Validate the resulting behavior or selected non-sensitive configuration, not a wholesale dump of environment variables into incident logs. Check the correct PID or invocation so a stale process observation is not mistaken for the replacement. Preserve enough information to identify which configuration version is active without turning troubleshooting into another disclosure channel.
Sources
- systemd 252 execution environment, environment-file parsing, precedence, optional paths, and per-process loading, checked 2026-09-12.
- systemd 252 systemctl, service reload versus daemon-reload and restart, checked 2026-09-12.
- systemd 252 service commands, ExecStart and ExecReload lifecycle, checked 2026-09-12.