
September 11, 2026
12 min read
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.
export, persisted in /etc/environment or shell profiles, and injected per-service via systemd Environment= directives.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.
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.
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.
| Location | Scope | Typical use | Seen by cron? | Seen by PHP-FPM? |
|---|---|---|---|---|
export in SSH | Current shell | Debugging | No | No |
~/.bashrc | Interactive user shell | Aliases, PATH tweaks | No | No |
~/.profile | User login | Composer, Node paths | Rarely | No |
/etc/environment | All PAM logins | Locale, global PATH | Sometimes | Only if inherited |
/etc/profile.d/*.sh | Login shells | SDK paths, Java | No | No |
systemd Environment= | One service | PHP, workers, agents | N/A | Yes (when set) |
Laravel .env | PHP app via Dotenv | DB, mail, API keys | Yes if cwd correct | Yes |
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.
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.
- Print from the same invocation path:
php -r 'print_r(getenv());'over HTTP versus CLI. - Inspect FPM:
grep -R "^env\[" /etc/php/8.3/fpm/pool.d/. - Inspect systemd:
systemctl show php8.3-fpm -p Environment. - Simulate cron:
env -i PATH=/usr/bin:/bin HOME=/home/deploy php artisan inspire. - 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/environmentfor global non-secret defaults; keep DB and API secrets in Laravel.envwith tight permissions. - Persist daemon-specific values with systemd
Environment=drop-ins or PHP-FPMenv[]pool directives. - Always set
PATHandSHELLin crontab headers before Laravelschedule:runlines. - Verify with
printenv,systemctl show, and/proc/PID/environ— not assumptions from your login shell. - Run
php artisan config:clearafter changing any variable Laravel caches viaconfig: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
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.

