Systemd service files support variable expansion through environment variables and the $HOME syntax, allowing for dynamic configuration without hardcoding paths. You can define variables using
Environment= directives or by sourcing external files with
EnvironmentFile=. This approach ensures that sensitive data like passwords or paths to user-specific directories remain flexible and secure across different deployment environments.
Defining Environment Variables in Service Units
The most direct method to introduce variables into a systemd service is via the
[Service] section. Using the
Environment= directive allows you to set key-value pairs that are available to the executed process. For instance, if you are running a web application that requires a specific log directory based on the current user, you might define it as follows:
[Service]
User=myuser
Group=mygroup
Environment=LOG_DIR=/var/log/myapp/$USER
ExecStart=/usr/bin/myapp --log-dir=$LOG_DIR
In this example, the shell-like expansion of
$USER happens at runtime. However, it is crucial to note that systemd does not expand variables inside the
ExecStart line itself using a shell. The variable must be expanded before the command is passed to the kernel. Therefore, relying on simple
Environment= definitions works well for static values, but complex logic often requires external scripts or environment files.
When defining multiple variables, each
Environment= line should contain one variable. While some versions of systemd allow space-separated pairs, standard practice dictates one variable per line to avoid parsing ambiguities. This clarity helps when troubleshooting why a variable appears empty during service startup.
Using External Environment Files
For more complex configurations, especially those involving secrets or large sets of variables, systemd provides the
EnvironmentFile= directive. This feature allows you to store variables in a separate text file, which systemd reads before starting the service. This separation keeps your unit file clean and makes it easier to manage sensitive information without exposing it in the service definition.
Create a file, such as
/etc/myapp/env.conf, with content like:
# /etc/myapp/env.conf
DB_PASSWORD=secret123
API_KEY=key_abc_xyz
WORKER_COUNT=4
Then, reference this file in your service unit:
[Service]
EnvironmentFile=/etc/myapp/env.conf
ExecStart=/usr/bin/myapp --workers=$WORKER_COUNT
By default, systemd expects these files to be readable only by root unless specified otherwise. If the file contains sensitive data, ensure its permissions are set strictly (e.g.,
chmod 600). Note that if the environment file is missing, systemd will fail to start the service by default. To make the file optional, prefix the path with a hyphen:
EnvironmentFile=-/etc/myapp/env.conf. The hyphen tells systemd to ignore errors if the file does not exist, allowing the service to start with default or unset variables.
This technique is particularly useful when integrating with other system components. For example, if you are managing storage mounts alongside your service, you might find similar patterns in guides about automatically mounting shares on Linux. See
Automatically Mounting Per-User Drives in Linux for context on how environment variables can interact with mount points and user sessions.
Variable Expansion Rules
It is important to understand how systemd handles variable expansion. Systemd uses a simple substitution mechanism. It replaces
$VARIABLE or
${VARIABLE} with the value defined in the environment. Unlike bash, systemd does not perform arithmetic expansion or command substitution within these strings.
If you need to use the output of a command as a variable value, you cannot do it directly in the
Environment= line. Instead, you must generate the value externally, perhaps via a wrapper script or a pre-start ExecStartPre command that writes to an environment file. This limitation prevents race conditions and security issues where a service might inadvertently execute arbitrary code during startup.
Common Pitfalls and Debugging
One common issue developers face is the belief that variables defined in
Environment= are expanded by a shell interpreter. They are not. The
ExecStart command is executed directly by systemd, bypassing the shell. If you write
ExecStart=/bin/bash -c "echo $VAR", the variable might not be expanded as expected because systemd passes the literal string to bash, which then interprets it. In many cases, the variable is already expanded by systemd before being passed to bash, but behavior can vary depending on quoting.
To debug variable issues, check the journal logs for the service. Use
journalctl -u your-service.service -f to watch real-time output. If a variable is critical, print its value at the very beginning of your application startup or in a wrapper script.
Another pitfall is the scope of variables. Variables defined in a service unit are only available to that specific service and its children. They do not propagate to the global system environment or to other services. If you need a variable available to all services, consider setting it in
/etc/environment or a profile script, though this is generally discouraged for security reasons.
For services that rely on complex dependencies or restart behaviors, understanding variable scope is vital. If a variable changes between restarts due to external factors, ensure your service is designed to handle dynamic updates. Refer to
Restarting Systemd Services Dependencies: A Tech Support Guide to understand how service state and environment interact during restart cycles.
Best Practices for Security and Maintenance
Always avoid hardcoding sensitive information like database passwords or API keys directly in the service file. Use
EnvironmentFile= with restricted permissions. Additionally, consider using systemd's built-in credential management features if available in your distribution, which offer encrypted storage for secrets.
When using variables, document them clearly in comments within the service file or the associated environment file. This documentation helps other administrators understand what variables are required and their expected formats. Regularly audit your service files to ensure that no deprecated variables are being used and that all referenced environment files still exist and are accessible.
Finally, test your service configuration in a staging environment before deploying to production. Verify that all variables are correctly expanded and that the service starts successfully under various conditions, including after reboots and updates. This proactive approach minimizes downtime and ensures a stable system configuration.
Frequently Asked Questions
Can I use shell commands to generate variable values in systemd?
No, systemd does not support command substitution like $(command) within Environment directives. You must generate the value externally using a script or a pre-start action that writes to an environment file.
How do I make an environment file optional so the service starts even if it is missing?
Prefix the path in the EnvironmentFile directive with a hyphen, like EnvironmentFile=-/path/to/file. This tells systemd to ignore errors if the file does not exist, preventing startup failures.
Why is my variable empty when I echo it inside a bash script called by ExecStart?
Ensure that the variable is defined in the same scope. If you are using a wrapper script, verify that the script inherits the environment from systemd. Sometimes, explicit export or passing variables as arguments is safer than relying on environment inheritance.
Is it safe to store passwords in plain text in an environment file?
It is better to restrict file permissions to root-only (chmod 600) and consider using systemd's credential management or external secret managers for high-security applications. Plain text storage carries inherent risks if file permissions are misconfigured.
Do variables defined in one service affect other services?
No, variables defined in a service unit are isolated to that service and its child processes. Each service has its own environment namespace, ensuring that configurations do not interfere with each other.