systemd services can be configured by passing environment variables, and the most surprising thing about this is how little you actually need to do to make it work.
Let’s see it in action. Imagine we have a simple Go application that reads an environment variable GREETING and prints "Hello, [GREETING]!".
package main
import (
"fmt"
"os"
)
func main() {
greeting := os.Getenv("GREETING")
if greeting == "" {
greeting = "World"
}
fmt.Printf("Hello, %s!\n", greeting)
}
We compile this into an executable, let’s call it greeter.
Now, let’s create a systemd service file for it, /etc/systemd/system/greeter.service:
[Unit]
Description=A simple greeter service
[Service]
ExecStart=/usr/local/bin/greeter
# Environment="GREETING=systemd User"
Restart=on-failure
[Install]
WantedBy=multi-user.target
Initially, if we start this service:
sudo systemctl start greeter
sudo systemctl status greeter
We’ll see:
● greeter.service - A simple greeter service
Loaded: loaded (/etc/systemd/system/greeter.service; disabled; vendor preset: enabled)
Active: active (running) since Tue 2023-10-27 10:00:00 UTC; 5s ago
Main PID: 12345 (greeter)
Tasks: 1 (limit: 4915)
Memory: 1.5M
CPU: 10ms
CGroup: /system.slice/greeter.service
└─12345 /usr/local/bin/greeter
Oct 27 10:00:00 myhost systemd[1]: Started A simple greeter service.
Oct 27 10:00:00 myhost greeter[12345]: Hello, World!
It defaults to "World" because GREETING wasn’t set.
To pass GREETING=systemd User, we uncomment and modify the Environment line in greeter.service:
[Unit]
Description=A simple greeter service
[Service]
ExecStart=/usr/local/bin/greeter
Environment="GREETING=systemd User"
Restart=on-failure
[Install]
WantedBy=multi-user.target
After reloading systemd and restarting the service:
sudo systemctl daemon-reload
sudo systemctl restart greeter
sudo systemctl status greeter
The output now shows:
● greeter.service - A simple greeter service
Loaded: loaded (/etc/systemd/system/greeter.service; disabled; vendor preset: enabled)
Active: active (running) since Tue 2023-10-27 10:05:00 UTC; 5s ago
Main PID: 12346 (greeter)
Tasks: 1 (limit: 4915)
Memory: 1.5M
CPU: 10ms
CGroup: /system.slice/greeter.service
└─12346 /usr/local/bin/greeter
Oct 27 10:05:00 myhost systemd[1]: Started A simple greeter service.
Oct 27 10:05:00 myhost greeter[12346]: Hello, systemd User!
This is the fundamental mechanism: systemd itself sets up the environment for the ExecStart process before it’s launched. It’s not a shell interpreting export, but systemd directly injecting these key-value pairs into the process’s environment block.
The problem this solves is providing dynamic configuration to services without modifying their source code or relying on external configuration files that the service itself must parse. systemd acts as a central configuration manager for the environment of your services.
Internally, when systemd starts a service, it constructs an environment for the ExecStart process. This environment is a list of null-terminated strings, each in the format KEY=VALUE. systemd populates this list based on several directives. The Environment= directive is the most straightforward, allowing you to specify variables directly.
Beyond Environment=, you can also use EnvironmentFile= to load variables from a file. This is useful for managing larger sets of configuration or keeping sensitive information separate. For example, if greeter.conf contains:
GREETING=File Based Config
OTHER_VAR=some_value
And greeter.service has:
[Service]
ExecStart=/usr/local/bin/greeter
EnvironmentFile=/etc/systemd/greeter.conf
Restart=on-failure
The service will pick up GREETING=File Based Config. Note that EnvironmentFile can contain comments (lines starting with #) and blank lines, which are ignored.
Crucially, if you specify multiple Environment= lines or use both Environment= and EnvironmentFile=, systemd merges them. Variables defined later in the unit file, or in later EnvironmentFile entries, will override earlier ones. The order matters.
The systemd environment is quite rich. It automatically includes standard variables like PATH, HOME, USER, LANG, and others that are set in the systemd environment itself when the system boots. You can override these or add your own.
One thing most people don’t know is how systemd handles the PATH variable specifically. By default, systemd services have a very minimal PATH, typically /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin. This is for security and to ensure services don’t accidentally pick up executables from unexpected locations. If your service needs a different PATH, you must explicitly set it using Environment="PATH=/path/to/your/bin:$PATH" or EnvironmentFile=. Forgetting this is a common reason why services fail to find executables that are readily available in an interactive shell.
The next concept you’ll run into is managing secrets and more complex configurations, often involving systemd-secret or integrating with external configuration management tools.