Ubuntu Environment Variables Explained

Khimananda Oli 8 min read Virtualization
Ubuntu Environment Variables Explained

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.

Environment Variable Resolution Hierarchy/etc/environmentSystem-Wide (PAM)~/.bashrc / ~/.profileUser Session Scopesystemd Environment=Service Unit ScopeInline / ExportProcess / Shell OnlyLower scopes override higher scopes; explicit exports beat inherited values
Ubuntu environment variables resolve in a strict hierarchy where service-level definitions typically override global system settings.

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 simple KEY=value syntax without export. Changes require a logout/login cycle or reboot to propagate to all sessions.
  • User-specific: Add export KEY=value to ~/.bashrc (interactive shells) or ~/.profile (login shells). This is ideal for developer tooling paths like GOROOT or NVM_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:

  1. EnvironmentFile directive (Recommended): Point to a protected file containing key-value pairs. Set permissions to 600 and ownership to the service user.
  2. Environment directive: Inline definitions directly in the unit file. Suitable for non-sensitive, static configuration.
  3. 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'.

Systemd Environment Injection FlowUnit FileEnvironment=KEY=valEnv File (600)/etc/svc/secrets.envsystemd ManagerMerges & ValidatesIsolated ProcessClean NamespaceShell profiles and /etc/environment are IGNORED by systemd services
Systemd constructs a fresh environment for each service, merging explicit unit directives and secured environment files while ignoring user shell state.

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 *.env and !.env.example to 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.env or 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/environmentsystemd EnvironmentFile
ScopeApplication process onlyAll user sessions (PAM)Single systemd unit
SyntaxKEY=value (app parser dependent)KEY=value (no export, no expansion)KEY=value or "KEY=value" (supports quoting)
Variable ExpansionDepends on app library (dotenv)NoLimited (supports % specifiers, not $VAR)
SecurityOften accidentally committedWorld-readable by defaultCan be restricted (chmod 600)
Reload MechanismApp restartLogout/Login or Rebootsystemctl daemon-reload + restart
Best ForLocal dev, Docker composeSystem paths, locale, proxyProduction 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.

Storage Method Trade-Off Matrix.env FilesDev Convenience: HighProd Security: LowPortability: HighUse for local dev & containers only/etc/environmentSystem Scope: GlobalFlexibility: LowSecret Safety: PoorReserve for locale, proxy, pathssystemd EnvFileIsolation: StrongAuditability: GoodProd Readiness: BestDefault for production services
Choosing the right Ubuntu environment variable storage method requires balancing convenience against security and isolation requirements.

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.

Frequently Asked Questions

Add the export statement to your ~/.bashrc or ~/.profile file for user-specific variables. For system-wide settings, create a custom script in /etc/profile.d/. Always run source on the modified file or log out and back in to apply changes immediately without rebooting.

The .profile loads once during login sessions and sets up the initial environment. The .bashrc executes for every new interactive non-login shell instance. Place PATH modifications in .profile and aliases or shell options in .bashrc to prevent duplicate entries and redundant processing overhead.

Run the printenv or env command to list all active variables. Use printenv VARIABLE_NAME to inspect a specific value. Both commands display the inherited environment for the current shell session, helping verify exports before running dependent applications or scripts.

Variables defined only with export in the terminal exist solely in that session. You must write them to a configuration file like ~/.bashrc, ~/.profile, or /etc/environment to survive reboots. Ensure no syntax errors prevent the file from sourcing correctly during initialization.

Avoid storing secrets directly in plaintext files readable by other users. Use restricted permissions (chmod 600) on config files or integrate a secret manager like HashiCorp Vault. Environment variables can leak through process listings, logs, or child processes, making dedicated secret storage safer for production credentials.

Remove the corresponding export line from your shell configuration files like ~/.bashrc or /etc/profile.d/ scripts. Then run unset VARIABLE_NAME to clear it from the current session. Simply unsetting without editing files will cause the variable to reappear after the next login or shell restart.

No, systemd services run in isolated environments and do not inherit user shell variables. Define required variables explicitly within the service unit file using the Environment= directive or reference an EnvironmentFile=. This ensures consistent behavior regardless of which user starts or manages the service.

This file uses simple KEY=VALUE pairs without the export keyword or shell expansion. It is parsed by PAM modules, not the shell, so avoid quotes unless they are part of the actual value. Changes apply globally at the next login for all users.

Check if the directory already exists before appending using parameter expansion or conditional logic in your shell config. A common pattern tests whether the path contains the new directory before modifying PATH. This prevents bloated paths and potential performance issues during command lookups across repeated shell invocations.

Yes, OS-level environment variables take precedence over Laravel .env values when using env() helpers. This allows deployment-specific overrides without modifying application code. However, cached configurations ignore runtime environment changes, so always run php artisan config:clear after updating server variables to ensure the application reads fresh values.

Use the -e flag with docker run or define them under environment: in docker-compose.yml. Alternatively, use --env-file to load multiple variables from a dedicated file. Container variables remain isolated from the host system, providing clean separation between Ubuntu host configuration and containerized application environments.

Sudo resets the environment by default for security reasons. Use sudo -E to explicitly preserve your current variables, or configure env_keep in /etc/sudoers for specific allowed variables. Be cautious, as passing untrusted variables to privileged contexts can introduce security vulnerabilities or unexpected behavior in administrative commands.

Cron runs with a minimal environment lacking user shell configurations. Define needed variables directly in the crontab or source your profile at the start of the job script. Redirect output to a log file to capture errors and verify the execution context matches expectations during troubleshooting.

The last assignment wins during shell initialization based on file sourcing order. System files load before user files, and later lines override earlier ones within the same file. Audit your configuration hierarchy carefully to identify unintended shadows causing applications to read stale or incorrect values unexpectedly.

Yes, variable names are strictly case sensitive in bash and most POSIX shells. HOME and home are treated as completely distinct identifiers. Follow uppercase conventions for standard variables to maintain readability and avoid accidental collisions with lowercase local variables used temporarily within scripts or functions.