19.5 Environment Variables and EnvironmentFile
Right, so you want to configure your service’s environment. You could, of course, just jam a bunch of Environment= lines into your unit file until it looks like a teenager’s first .bashrc. That works, but it’s messy and a pain to maintain. The designers of systemd, in a rare moment of clarity, gave us a better way: the EnvironmentFile.
Let’s be real, though. The name EnvironmentFile is a bit of a misnomer. It doesn’t set the environment from a file; it reads environment variables from a file. It’s a subtle but important distinction that will bite you later if you don’t understand it. I’ll get to that.
The Simple, Inline Way: Environment=
For one-off variables or a very small set, you can just define them directly in the service unit. It’s straightforward, if a bit clunky.
[Service]
Type=simple
ExecStart=/usr/bin/my-awesome-server
Environment=PORT=3000
Environment=LOG_LEVEL=debug NODE_ENV=production
Restart=on-failure
Notice you can put one variable per line (Environment=PORT=3000) or stack them, space-separated, on a single line (Environment=LOG_LEVEL=debug NODE_ENV=production). The space-separated syntax is a gift from the POSIX gods, making it slightly less verbose. Use it.
Why would you use this? It’s perfectly fine for a variable or two. It keeps the configuration self-contained. The massive downside is that if you need to share these variables with another service (like a companion script), you’re now maintaining the same list in two places. Don’t do that. That way lies madness and typos.
The Grown-Up Way: EnvironmentFile=
This is the method you actually want for anything non-trivial. You point systemd to a file—conventionally placed in /etc/default/ or /etc/sysconfig/—that contains your key-value pairs.
First, create your environment file. The format is dead simple; it’s just KEY=VAL lines. Let’s make /etc/default/my-awesome-server.
# This is a comment. systemd will ignore it.
PORT=3000
LOG_LEVEL=debug
NODE_ENV=production
SECRET_KEY="This is a string with spaces, so it needs quotes"
MULTILINE="This is a string that\nhas a newline in it. Seriously."
Now, tell your service unit about it.
[Service]
Type=simple
ExecStart=/usr/bin/my-awesome-server
EnvironmentFile=/etc/default/my-awesome-server
Restart=on-failure
Beautiful. Clean. Maintainable. You can now update variables in one place without touching the unit file. You can even source this same file from a shell script if you need to. This is the way.
Now, here’s the critical pitfall everyone falls into: The EnvironmentFile is not a shell script. systemd does not source this file with bash. It parses it with its own… let’s call it “creative”… parser. This means:
- No variable interpolation.
PATH=$HOME/bin:$PATHwill literally setPATHto the string"$HOME/bin:$PATH". It will not expand$HOME. This is the most common “why isn’t this working?!” moment. - No export needed. The
exportkeyword is meaningless here and will likely cause an error. Just useKEY=VAL. - Quoting is… weird. The systemd parser understands basic shell-like quoting, but don’t push your luck. For strings with spaces, quotes are your friend. For multiline values like our
MULTILINEexample above, you can use the\nescape sequence. It’s not elegant, but it works.
The Order of Operations (This Matters)
Here’s another “fun” quirk. You can use both Environment= and EnvironmentFile= in the same unit. Which one wins? The answer is: the last one loaded.
systemd processes these directives in the order they appear in the unit file. Later declarations override earlier ones. This is most relevant when you have multiple EnvironmentFile lines or a mix of both methods.
[Service]
EnvironmentFile=/etc/default/my-service # Sets FOO=bar
Environment=FOO=overtridden # FOO is now 'overridden'
EnvironmentFile=-/etc/default/my-service-overrides # This file's FOO would win again
See that dash in EnvironmentFile=-/path/to/file? That’s a best practice. The - prefix tells systemd it’s okay if the file doesn’t exist. It won’t throw an error. This is incredibly useful for optional override files.
The Nuclear Option: PassEnvironment=
Sometimes, you just want your service to inherit a specific variable from the systemd manager’s own environment. Maybe it’s JAVA_HOME or PATH. For that, use PassEnvironment.
[Service]
...
PassEnvironment=JAVA_HOME PATH
This tells systemd: “Hey, take whatever you have for JAVA_HOME and PATH right now and pass it through to the executed process.” This is useful for ensuring a service respects a system-wide configuration set elsewhere. Use it sparingly, as it makes your service’s environment less explicit and more dependent on the state of the systemd daemon.
So, what’s the verdict? Use Environment= for one-offs. Use EnvironmentFile= for almost everything else. And for the love of all that is holy, remember it’s not a shell script. You’ll save yourself hours of confused head-scratching.