Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Ubuntu Environment Variables Explained

By Kokil Thapa | Last reviewed: September 2026

Your Laravel app reads DB_HOST from .env, but cron still fails with “connection refused.” That gap is almost always Ubuntu environment variables — not application code. On a production VPS, the same variable can exist in your SSH session, vanish inside a systemd unit, and never reach PHP-FPM at all. This guide maps where Ubuntu stores variables, how login shells load them, and the patterns I use on real Ubuntu server setups for PHP 8.3+, Nginx, MySQL 9.7, and Deployer 7 releases.

What are Ubuntu environment variables and how do they work?

An environment variable is a named value that child processes inherit from their parent. When you run php artisan migrate, the PHP binary sees PATH, HOME, and anything you exported in that shell. Ubuntu does not keep one global table every program reads. Each process gets its own copy at fork time.

The shell is the usual entry point on servers. Bash reads profile scripts, exports names, then execs your command. Systemd services skip your .bashrc entirely unless you wire variables into the unit file. That split causes most “it works when I SSH in” production bugs.

Ubuntu Environment Variable LayersShell Sessionexport VAR=valueUser Profile~/.profileSystem Wide/etc/environmentsystemd ServiceUnit Environment=Each layer feeds different processesSSH, cron, PHP-FPM, queue workers
Ubuntu environment variables explained: shell exports, user profiles, system files, and systemd units each target different runtimes.

Common variables on web servers include PATH (executable lookup), HOME, USER, LANG, and app-specific names like APP_ENV. Laravel 12 and 13 apps normally read secrets from .env via vlucas/phpdotenv, not from OS variables — unless you deliberately map them in config/*.php with env('KEY').

Inspect what a running process actually sees:

echo "$PATH"
printenv | sort
printenv DB_HOST
cat /proc/$(pgrep -n php-fpm8.3)/environ | tr '\0' '\n' | sort

The last command is the ground truth for PHP-FPM. If DB_HOST is missing there but present in your SSH session, you found the bug. For broader server context, see the essential Ubuntu terminal commands reference and my notes on Ubuntu user management.

How do you set environment variables temporarily on Ubuntu?

Temporary assignment lasts only for the current shell or single command. Use this for quick tests before you persist anything.

Export in the current Bash session

export APP_ENV=local
export PATH="/usr/local/bin:$PATH"
php artisan config:show app.env

export marks the name for child processes. Without it, the variable stays shell-local and PHP spawned from that shell will not see it.

Inline for one command

APP_ENV=staging php artisan migrate --force
env APP_DEBUG=true php artisan route:list

This pattern is ideal for CI scripts and one-off diagnostics. It avoids polluting the session.

Unset and verify

unset APP_ENV
declare -p APP_ENV 2>/dev/null || echo "APP_ENV is not set"

On production boxes I maintain with Linux system administration workflows, I treat temporary exports as experiments only. Persist the winner in the correct file once verified.

How do you make environment variables persistent on Ubuntu?

Persistence means the variable appears every time the relevant context starts — login shell, all users, or a specific daemon. Pick the narrowest scope that still covers every runtime that needs the value.

Login Shell Load Order/etc/profile/etc/profile.d/*.sh~/.profile~/.bashrcNon-login shells read only ~/.bashrcCron and systemd skip both by defaultUse crontab PATH= or systemd Environment=
Ubuntu environment variables explained through Bash profile order — and why cron jobs often miss values you set in ~/.bashrc.

System-wide: /etc/environment

Ubuntu reads /etc/environment through PAM at login. Syntax is strict: KEY="value" pairs, no export keyword. Official guidance lives in the Ubuntu environment(5) man page.

sudo nano /etc/environment

Example contents:

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
LANG="en_US.UTF-8"
JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"

Log out and back in, or reboot, before expecting desktop sessions to pick up changes. Daemons may still ignore this file unless started through PAM-aware login paths.

Per-user: ~/.profile and ~/.bashrc

Put login-wide settings in ~/.profile. Interactive shell tweaks belong in ~/.bashrc. A pattern I use on deploy users:

# ~/.profile
export PATH="$HOME/.composer/vendor/bin:$PATH"
export EDITOR=nano

Reload without logging out:

source ~/.profile

For shared snippets across projects, drop scripts into /etc/profile.d/appname.sh with mode 644. Details on permissions sit in the Ubuntu file permissions guide.

systemd units and drop-ins

PHP-FPM, Nginx, Redis 8.10, and queue workers run under systemd. Their environment comes from unit files, not your shell. The systemd Environment= directive is the authoritative reference.

sudo systemctl edit php8.3-fpm

Add:

[Service]
Environment="APP_ENV=production"
Environment="PATH=/usr/bin:/bin"

Then reload:

sudo systemctl daemon-reload
sudo systemctl restart php8.3-fpm

Prefer drop-in fragments over editing vendor unit files directly. Package upgrades overwrite the base unit.

How do system-wide and user-scoped environment variables differ on Ubuntu?

Scope controls blast radius. System-wide variables in /etc/environment affect every PAM login on the box. User-scoped entries in ~deploy/.profile affect only that account’s shells. Service-scoped entries affect only that daemon’s children.

LocationScopeTypical useSeen by cron?Seen by PHP-FPM?
export in SSHCurrent shellDebuggingNoNo
~/.bashrcInteractive user shellAliases, PATH tweaksNoNo
~/.profileUser loginComposer, Node pathsRarelyNo
/etc/environmentAll PAM loginsLocale, global PATHSometimesOnly if inherited
/etc/profile.d/*.shLogin shellsSDK paths, JavaNoNo
systemd Environment=One servicePHP, workers, agentsN/AYes (when set)
Laravel .envPHP app via DotenvDB, mail, API keysYes if cwd correctYes
Persistence Strategy PickerTemporary exportTesting onlyLost on exitProfile filesDeploy user shellsNot for FPMsystemd unitDaemons and workersSurvives rebootApp secrets: Laravel .env in shared/OS vars for PATH, locale, JVM — not DB passwordsValidate with printenv and /proc/PID/environ
Pick the narrowest persistence layer when Ubuntu environment variables must reach shells, cron, or PHP-FPM.

Security follows scope. Never put database passwords in /etc/environment where every user account can read them. Keep secrets in .env with mode 600 owned by the deploy user. Pair that with the server hardening for Ubuntu web servers checklist and UFW firewall configuration basics.

Where should you put environment variables for PHP, Nginx, and Laravel on Ubuntu?

Web stacks touch three runtimes: FPM workers handling HTTP, CLI invoked by cron or Deployer, and occasionally queue workers as separate systemd services. Each may see a different environment.

PHP-FPM pool environment

After following PHP installation on Ubuntu, set pool-level variables when only PHP needs them:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf
env[APP_ENV] = production
env[PATH] = /usr/local/bin:/usr/bin:/bin

Restart FPM after edits. Pool env[] directives append to the worker environment. They do not replace Laravel’s .env loader unless your code calls getenv() directly.

Nginx and fastcgi_param

Nginx does not export shell variables to PHP by default. You pass selected values through FastCGI params after installing Nginx on Ubuntu:

fastcgi_param APP_RUNTIME env:APP_ENV;

Most Laravel apps never need this. Standard try_files + fastcgi_pass to PHP-FPM is enough when .env lives in the release or shared path.

Laravel .env versus OS variables

Laravel resolves env('KEY') at bootstrap from .env unless you run php artisan config:cache. Cached config bakes values at cache time. Changing OS variables afterward does nothing until you rebuild the cache.

On Deployer 7 layouts I use for sister legal-tech sites, .env symlinks from shared/.env into each release. OS-level PATH must include Composer and PHP binaries for CLI tasks either way.

cd /var/www/example/current
php artisan config:clear
php artisan config:cache

For database setup context see MySQL installation on Ubuntu. For deployment sequencing, the Symfony deployment on Ubuntu VPS guide mirrors the same env discipline Symfony 8.1 and Laravel share.

Cron and scheduled tasks

Cron jobs run under /usr/sbin/cron with a minimal environment. A Laravel scheduler line that works in SSH often fails silently in crontab. Fix it explicitly:

crontab -e
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
* * * * * cd /var/www/app/current && php artisan schedule:run >> /dev/null 2>&1

Or call a wrapper script that sources profile and logs output. The Ubuntu cron jobs guide covers logging patterns. I log to storage/logs/cron.log on production Laravel apps so failures show up before clients notice.

Cron vs SSH EnvironmentSSH sessionReads ~/.profileFull PATH setCron jobMinimal PATHNo bashrcphp artisan OKmigrate succeedsphp not foundschedule silent failFix: set PATH in crontab header
Ubuntu environment variables explained for cron — the scheduler pitfall behind many Laravel production incidents.

Queue workers deserve the same treatment. Define a systemd unit with WorkingDirectory=, User=deploy, and explicit Environment= lines rather than assuming login profiles run. On booking platforms like Adventure Third Pole Trek, missed queue env vars stalled confirmation emails until systemd drop-ins were added.

How do you debug missing environment variables on Ubuntu?

Start by identifying which process actually failed: FPM worker, artisan CLI, cron, or a GitLab CI runner shell. Compare environments side by side instead of guessing.

  1. Print from the same invocation path: php -r 'print_r(getenv());' over HTTP versus CLI.
  2. Inspect FPM: grep -R "^env\[" /etc/php/8.3/fpm/pool.d/.
  3. Inspect systemd: systemctl show php8.3-fpm -p Environment.
  4. Simulate cron: env -i PATH=/usr/bin:/bin HOME=/home/deploy php artisan inspire.
  5. Clear Laravel config cache if values look stale: php artisan config:clear.

For JSON-heavy API debugging, paste config dumps into the JSON formatter tool to diff outputs quickly. Shell scripts benefit from the same discipline described in the Ubuntu shell scripting tutorial and Bash scripting guide.

GitLab CI variables are yet another layer. Pipeline env vars do not propagate to the server after Deployer runs unless you write them into .env or server files during deploy. Treat CI secrets and server secrets as related but separate concerns — similar to the split covered in Terraform variables and outputs for infrastructure repos.

When upgrades change PHP binary paths, revisit every crontab and systemd unit that hard-coded the old version. PHP 8.3 to 8.5 migrations on Ubuntu 24.04 have broken scheduled jobs on client servers because /usr/bin/php pointed elsewhere after alternatives changed. Pin absolute paths in production cron lines when you cannot risk drift.

Backups and monitoring scripts need env vars too. A nightly mysqldump cron that reads ~/.my.cnf still needs HOME set correctly. See Ubuntu server backup strategies and server monitoring guide for wrapper examples. Security-sensitive boxes should align with Ubuntu security hardening and regular security updates.

Key Takeaways

  • Ubuntu environment variables are per-process copies — SSH exports do not automatically reach PHP-FPM, cron, or queue workers.
  • Use /etc/environment for global non-secret defaults; keep DB and API secrets in Laravel .env with tight permissions.
  • Persist daemon-specific values with systemd Environment= drop-ins or PHP-FPM env[] pool directives.
  • Always set PATH and SHELL in crontab headers before Laravel schedule:run lines.
  • Verify with printenv, systemctl show, and /proc/PID/environ — not assumptions from your login shell.
  • Run php artisan config:clear after changing any variable Laravel caches via config:cache.

People Also Ask

What is the difference between export and setenv on Ubuntu?

Bash uses export NAME=value to mark shell variables for child processes. C programs call setenv() from libc to modify the environment of the current process and its children. Both achieve the same outcome at the process level; you interact with export in terminal sessions and shell scripts on Ubuntu.

Does Ubuntu read .env files automatically?

No. The kernel and shell ignore .env files completely. Laravel, Symfony, Docker Compose, and similar tools load them explicitly through their own bootstrap code. On a bare Ubuntu server, only files like /etc/environment, profile scripts, and systemd unit directives define OS-level variables.

Where is PATH defined by default on Ubuntu 24.04?

Default PATH comes from /etc/environment at PAM login and is extended by /etc/profile, /etc/profile.d/*.sh, and user profiles. Systemd services receive a minimal PATH unless you override it in the unit. Check live values with echo "$PATH" in each context you care about.

Can you use environment variables in sudo commands?

sudo resets most environment variables for security via env_reset in /etc/sudoers. Pass preserved variables with sudo VAR=value command or list allowed names using Defaults env_keep += "VAR" in a sudoers drop-in under /etc/sudoers.d/. Never preserve secrets broadly.

Put Ubuntu environment variables on a stable footing

Once you map which layer owns each name, “works in SSH but not in production” stops being a mystery. Keep OS variables for paths, locale, and tooling; keep application secrets in .env; wire cron and systemd explicitly. That is the baseline behind every reliable Laravel 12/13 and PHP 8.3+ deployment I maintain. If you want help auditing a live VPS — cron, FPM pools, Deployer releases, and all — reach out through contact us or review support and maintenance services. For related reading, browse the blog index, the home page, or my about page for background on how these stacks are operated day to day.

Frequently Asked Questions

Ubuntu environment variables are name=value pairs copied into each process at fork time, not stored in one global table every program reads. When Bash starts, it loads profile scripts, exports names, then execs your command. Child processes inherit what their parent exported. Systemd services skip .bashrc unless you wire variables into unit files. That split is why the same variable can exist in SSH but never reach PHP-FPM or cron.

Use export NAME=value in the current Bash session so child processes see it, or assign inline for one command only: APP_ENV=staging php artisan migrate --force. Without export, the variable stays shell-local and PHP spawned from that shell will not see it. Unset with unset APP_ENV. On production VPS boxes I treat temporary exports as experiments only and persist verified values in the correct file afterward.

Pick the narrowest scope that covers every runtime needing the value. System-wide defaults go in /etc/environment using KEY="value" syntax with no export keyword. Per-user login settings belong in ~/.profile; interactive tweaks in ~/.bashrc. Shared snippets can live in /etc/profile.d/appname.sh with mode 644. Daemons like PHP-FPM need systemd drop-ins via sudo systemctl edit php8.3-fpm with Environment= lines, then daemon-reload and restart. Log out or reboot after /etc/environment changes before expecting desktop sessions to pick them up.

Scope controls blast radius. /etc/environment affects every PAM login on the box. ~/.profile affects only that account's shells. systemd Environment= affects only one service's children. Cron rarely sees ~/.bashrc or ~/.profile values. PHP-FPM sees systemd and pool env[] directives, not SSH exports. Laravel .env reaches PHP when the working directory is correct. Pick the narrowest persistence layer. Never put database passwords in /etc/environment where every user account can read them.

Web stacks touch three runtimes that may see different environments: FPM workers, CLI invoked by cron or Deployer, and queue workers as separate systemd services. Set pool-level values in /etc/php/8.3/fpm/pool.d/www.conf using env[APP_ENV] = production. Most Laravel apps need no Nginx fastcgi_param overrides when .env lives in the release path. Keep application secrets in Laravel .env with mode 600 owned by the deploy user. On Deployer 7 layouts, .env symlinks from shared/.env into each release.

Cron runs under /usr/sbin/cron with a minimal environment and does not load your login shell profiles. A schedule:run line that succeeds interactively often fails silently in crontab because PATH, SHELL, and working directory differ. Fix it by setting SHELL=/bin/bash and PATH=/usr/local/bin:/usr/bin:/bin in the crontab header, then cd to the app release before php artisan schedule:run. Log output to storage/logs/cron.log so failures surface before clients notice. Queue workers need the same explicit treatment via systemd units.

Identify which process failed—FPM worker, artisan CLI, cron, or a CI runner—and compare environments side by side. Run php -r 'print_r(getenv());' over HTTP versus CLI. Inspect FPM pool config with grep -R "^env\[" /etc/php/8.3/fpm/pool.d/. Check systemd with systemctl show php8.3-fpm -p Environment. Simulate cron with env -i PATH=/usr/bin:/bin HOME=/home/deploy php artisan inspire. Ground truth for a running FPM worker is cat /proc/$(pgrep -n php-fpm8.3)/environ. Clear Laravel config cache if values look stale.

Bash export marks shell variables for child processes. C programs use libc setenv() instead. Same outcome at the process level.

No. The kernel and shell ignore .env files. Laravel loads them via vlucas/phpdotenv at bootstrap.

Default PATH comes from /etc/environment at PAM login, extended by /etc/profile, /etc/profile.d/*.sh, and user profiles. Systemd services get a minimal PATH unless overridden in the unit file.

PHP-FPM runs under systemd and skips your shell profiles entirely. systemd Environment= drop-ins are the authoritative way to inject variables into FPM workers. Prefer sudo systemctl edit php8.3-fpm over editing vendor unit files directly, because package upgrades overwrite base units. Pool-level env[] directives in www.conf work when only PHP needs a value. Shell profiles suit interactive deploy-user sessions and Composer paths, not daemon children. I use both layers deliberately: profiles for SSH CLI work, systemd for FPM and queue workers.

Laravel resolves env('KEY') at bootstrap from .env unless you run php artisan config:cache, which bakes values at cache time. Changing OS variables afterward does nothing until you rebuild the cache with config:clear then config:cache. OS variables suit PATH, locale, and tooling paths. Application secrets belong in .env, not /etc/environment. OS-level PATH must still include Composer and PHP binaries for CLI tasks invoked by cron or Deployer 7 regardless of what .env contains.

sudo resets most environment variables for security via env_reset in /etc/sudoers. Pass a single preserved variable with sudo VAR=value command, or allow specific names using Defaults env_keep += "VAR" in a drop-in under /etc/sudoers.d/. Never preserve secrets broadly across sudo sessions. On hardened production boxes, treat sudo env handling as part of your security baseline alongside UFW and tight .env permissions rather than a convenience shortcut for deployment scripts.

Keep database passwords and API keys in Laravel .env with mode 600 owned by the deploy user, not in /etc/environment where every account can read them. Use the narrowest scope: system-wide files for locale and global PATH only, systemd or pool directives for service-specific non-secret values. Align sudo env_keep with least privilege. Pair file permissions with the server hardening checklist and regular security updates. GitLab CI pipeline variables are a separate layer—they do not propagate to the server after Deployer runs unless written into .env or server files during deploy.

Laravel config:cache bakes env('KEY') values into cached config files at cache time. After that, changing OS variables or even .env has no effect until you run php artisan config:clear and rebuild the cache. This catches teams who fix DB_HOST in a systemd drop-in but still see connection errors because production runs cached config from an earlier deploy. Always config:clear after changing any variable Laravel caches. On Deployer 7 releases, verify the shared .env symlink and rebuild config as part of your deploy sequence, not only when debugging.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: