
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Configuring application behavior without hardcoding values is a fundamental skill for any reliable infrastructure, yet misconfigured Ubuntu environment variables remain a top cause of deployment failures and security leaks. Whether you are deploying a Laravel app on a VPS or orchestrating microservices, understanding how the shell resolves these values prevents subtle bugs that only appear in production. This guide cuts through the theory to show you exactly how to manage, persist, and secure environment state on modern Ubuntu systems.
export KEY=value, persist them via /etc/environment or user profiles like .bashrc, and inject them securely into services using systemd unit files or dedicated secrets managers rather than plaintext files.How do you set and persist Ubuntu environment variables correctly?
The most common mistake engineers make is confusing temporary shell variables with persistent system configuration. When you run export DB_HOST=localhost in a terminal, that variable exists only for the current shell session and its children. The moment you close the terminal or restart the server, it vanishes. For production infrastructure, you need persistence strategies that survive reboots and user logouts.
Temporary vs. Persistent Configuration
Use inline exports for debugging, testing, or one-off scripts. For permanent configuration, choose the scope carefully based on who needs access to the variable:
- System-wide (all users): Edit
/etc/environment. This file is parsed by PAM modules during login. Use simpleKEY=valuesyntax withoutexport. Changes require a logout/login cycle or reboot to propagate to all sessions. - User-specific: Add
export KEY=valueto~/.bashrc(interactive shells) or~/.profile(login shells). This is ideal for developer tooling paths likeGOROOTorNVM_DIR. - Service-specific: Define variables directly in systemd unit files using the
Environment=directive. This isolates configuration to the service and prevents leakage to interactive shells.
# System-wide persistence (/etc/environment)
DATABASE_URL="postgres://app:[email protected]:5432/prod"
REDIS_HOST="cache.internal"
# User-level persistence (~/.bashrc)
export PATH="$HOME/.local/bin:$PATH"
export EDITOR="vim"
# Service-level persistence (systemd unit)
[Service]
Environment="APP_ENV=production"
Environment="LOG_LEVEL=warn" A critical nuance often missed in tutorials: /etc/environment does not support variable expansion. You cannot write PATH=$PATH:/opt/app/bin there. If you need expansion, use /etc/profile.d/custom.sh instead, which is sourced as a shell script during login. For automated server provisioning, I recommend managing these files via Ansible or cloud-init to ensure consistency across your fleet, as detailed in my guide on automating server setup with Ansible playbooks.
Why aren't my Ubuntu environment variables visible in systemd services?
This is the single most frequent issue I troubleshoot when teams migrate from legacy init scripts to systemd. Services managed by systemd do not inherit environment variables from your shell profile, /etc/environment, or even the root user's session. Systemd creates a clean, isolated execution environment for each unit to ensure reproducibility and security.
Injecting Variables into Systemd Units
You must explicitly declare every variable a service needs. There are three primary methods, ranked by security and maintainability:
- EnvironmentFile directive (Recommended): Point to a protected file containing key-value pairs. Set permissions to
600and ownership to the service user. - Environment directive: Inline definitions directly in the unit file. Suitable for non-sensitive, static configuration.
- Credential encryption: Use systemd-creds or LoadCredentialEncrypted for sensitive data on systemd v250+. This avoids storing secrets in plaintext entirely.
[Unit]
Description=My Application Service
After=network.target
[Service]
Type=simple
User=appuser
# Method 1: Secure external file
EnvironmentFile=/etc/myapp/env.production
# Method 2: Inline non-sensitive config
Environment="PORT=8080"
Environment="NODE_ENV=production"
ExecStart=/usr/local/bin/myapp serve
[Install]
WantedBy=multi-user.target After modifying a unit file or its environment file, always run sudo systemctl daemon-reload followed by sudo systemctl restart myapp. A common pitfall is forgetting the daemon-reload; systemd will continue using the cached old configuration silently. Verify the active environment of a running service with systemctl show myapp --property=Environment or inspect the full runtime state via cat /proc/$(systemctl show myapp --property=MainPID --value)/environ | tr '\0' '\n'.
How do you securely manage sensitive Ubuntu environment variables in production?
Storing database passwords, API keys, or encryption tokens in /etc/environment or .bashrc is a critical security anti-pattern. These files are often world-readable, backed up in plaintext, and logged by configuration management tools. In compliance-focused environments (SOC 2, ISO 27001), this practice alone can fail an audit. Treat secrets differently from configuration.
Secure Storage Patterns
For standalone Ubuntu servers without a secrets manager, use restricted-permission environment files owned exclusively by the service account:
# Create secure secret store
sudo mkdir -p /etc/myapp
sudo touch /etc/myapp/secrets.env
sudo chown myapp:myapp /etc/myapp/secrets.env
sudo chmod 600 /etc/myapp/secrets.env
# Content of secrets.env (no export keyword needed for EnvironmentFile)
DB_PASSWORD="s3cur3_p@ssw0rd!"
JWT_SECRET="eyJhbGciOiJIUzI1NiIs..."
AWS_SECRET_ACCESS_KEY="AKIA..." For teams scaling beyond a handful of servers, integrate HashiCorp Vault or AWS Secrets Manager. Applications fetch secrets at runtime via API or use the Vault Agent to render templated env files with automatic rotation. This eliminates static secrets on disk entirely. When working with containerized workloads, never bake secrets into images; instead, inject them at orchestration time. My article on secrets management with HashiCorp Vault covers architectural patterns for both VM and Kubernetes deployments.
Avoiding Common Security Pitfalls
- Never commit .env files to Git. Add
*.envand!.env.exampleto your.gitignore. Provide a template with dummy values instead. - Audit access logs. Enable auditd rules for sensitive env files to track who reads or modifies them.
- Sanitize error output. Ensure applications don't dump
process.envor stack traces containing secrets to logs or client responses. - Rotate regularly. Static env-file secrets should have documented rotation procedures. Automate this where possible.
What is the difference between .env files, /etc/environment, and systemd EnvironmentFile?
These three mechanisms look similar but serve fundamentally different purposes and have distinct parsing behaviors. Confusing them leads to variables that work in development but fail silently in production services.
| Feature | .env (App-Level) | /etc/environment | systemd EnvironmentFile |
|---|---|---|---|
| Scope | Application process only | All user sessions (PAM) | Single systemd unit |
| Syntax | KEY=value (app parser dependent) | KEY=value (no export, no expansion) | KEY=value or "KEY=value" (supports quoting) |
| Variable Expansion | Depends on app library (dotenv) | No | Limited (supports % specifiers, not $VAR) |
| Security | Often accidentally committed | World-readable by default | Can be restricted (chmod 600) |
| Reload Mechanism | App restart | Logout/Login or Reboot | systemctl daemon-reload + restart |
| Best For | Local dev, Docker compose | System paths, locale, proxy | Production service configuration |
In practice, reserve /etc/environment for truly universal settings like LANG, http_proxy, or system-wide Java home paths. Use application-level .env files only in development or within containers where the entrypoint explicitly sources them. For everything else on a production Ubuntu server, systemd EnvironmentFile with strict permissions is the correct default. Understanding these distinctions prevents the "works on my machine" class of deployment failures that plague teams transitioning from local development to managed infrastructure.
Mastering Ubuntu Environment Variables for Reliable Infrastructure
Getting Ubuntu environment variables right is less about memorizing syntax and more about understanding scope, lifecycle, and security boundaries. Always match the storage mechanism to the variable's sensitivity and consumer: global paths belong in /etc/environment, developer tooling in shell profiles, and production service configuration in secured systemd environment files. Never store secrets in plaintext where they can leak through backups, logs, or version control. Audit your current setup today by checking /proc/*/environ for exposed credentials and verifying that critical services define their dependencies explicitly rather than inheriting ambient shell state. If your team needs help establishing compliant, audit-ready configuration management practices across your Ubuntu fleet, reach out to discuss your infrastructure requirements.