Skip to main content

Configuring Linux Environment — Variables, PATH, Dotfiles, and Shell Startup

Master Linux environment variables, PATH management, dotfile configuration, shell startup file ordering, and direnv for consistent production and development environments.

30 Terms

Overview and What You Will Learn

The Linux environment is the invisible configuration layer that determines how every command behaves. Which Python runs when you type python3? What editor opens when you run git commit? Which directories are searched for commands? All of this comes from environment variables set in shell startup files.

By the end of this lab you will:

  • Understand the difference between shell variables and environment variables
  • Trace exactly which startup files are sourced in each shell context
  • Configure PATH correctly for interactive shells, scripts, and services
  • Write clean, maintainable dotfiles for team-wide shell configuration
  • Use direnv for per-project environment isolation
  • Configure system-wide environment variables for all users and services

Why This Matters in Production

A Zerodha DevOps engineer's deployment script uses python3 for validation. On their laptop it runs Python 3.11. On the production server the same script runs Python 3.6 because the PATH is configured differently. The script silently produces different results. Understanding environment configuration means this kind of environment drift is diagnosed and prevented rather than discovered in production.

Core Principles

Shell startup file sourcing order:

◈ DIAGRAM
Login shell (ssh session, su -):
+------------------------------------------+
| /etc/profile | <- system-wide, all users
+------------------------------------------+
|
v
+------------------------------------------+
| /etc/profile.d/*.sh | <- drop-in system config
+------------------------------------------+
|
v
+------------------------------------------+
| ~/.bash_profile OR ~/.profile | <- user-specific login config
| (sources ~/.bashrc if it exists) |
+------------------------------------------+
|
v
+------------------------------------------+
| ~/.bashrc | <- interactive shell config
+------------------------------------------+
Non-login interactive shell (new terminal in existing session):
Sources: ~/.bashrc only
Non-interactive shell (scripts, cron):
Sources: nothing (BASH_ENV if set)
Result: minimal PATH, no aliases, no functions

Detailed Step-by-Step Practical Lab

Milestone 1 — Understand shell vs environment variables

Bash
## Shell variable (local to current shell)
MY_VAR="hello"
echo $MY_VAR ## hello
bash -c 'echo $MY_VAR' ## (empty -- not in child)
## Environment variable (inherited by child processes)
export MY_VAR="hello"
bash -c 'echo $MY_VAR' ## hello
## See all environment variables
env
printenv
## See all variables (shell + environment)
set | head -30
## Check if a variable is exported (in environment)
export -p | grep MY_VAR
## declare -x MY_VAR="hello"
## Set variable for ONE command only
NODE_ENV=staging node server.js
## After command, NODE_ENV reverts
## Unset a variable
unset MY_VAR
## Make a variable read-only
readonly DB_HOST="10.0.2.100"
DB_HOST="other" ## Error: readonly variable

Milestone 2 — Trace the startup file order

Bash
## Add debug tracing to find which files are sourced
## Temporarily add to /etc/profile:
echo "DEBUG: /etc/profile sourced at $(date)" >> /tmp/startup-trace.log
## Check what your login shell sources
## Run a login shell and trace
bash --login -x 2>&1 | grep -E 'source|\.'
## Check which shell type you are in
## Login shell: $0 starts with -
echo $0
## -bash <- login shell
## bash <- non-login shell
## Test what environment a cron job sees
## Add this to crontab temporarily:
## * * * * * env > /tmp/cron-env-$(date +%s).txt
## Then compare with your interactive environment:
diff <(env | sort) <(cat /tmp/cron-env-*.txt | sort)
## Test what a systemd service sees
## Run the service environment via systemctl
sudo systemctl show payment-api --property=Environment
sudo cat /proc/$(systemctl show payment-api -p MainPID --value)/environ | tr '\0' '\n'

Milestone 3 — Manage PATH correctly

Bash
## View current PATH
echo $PATH
## /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
## Add to PATH (prepend -- your tools take priority)
export PATH="/opt/mytools/bin:$PATH"
## Add to PATH (append -- fallback if not found elsewhere)
export PATH="$PATH:/opt/mytools/bin"
## Add permanently for your user
cat >> ~/.bashrc << 'EOF'
## Custom tools
export PATH="/opt/mytools/bin:$HOME/.local/bin:$PATH"
EOF
source ~/.bashrc ## apply to current session
## Add system-wide (all users)
sudo tee /etc/profile.d/custom-tools.sh << 'EOF'
export PATH="/opt/mytools/bin:$PATH"
EOF
## Which binary wins?
which python3
## /usr/bin/python3
type -a python3
## python3 is /usr/local/bin/python3 <- checked first
## python3 is /usr/bin/python3
## Debug path issues
command -v node
## /usr/local/bin/node
## If empty: node is not in PATH
## Check exact PATH for a running service
sudo cat /proc/$(pgrep payment-api | head -1)/environ | tr '\0' '\n' | grep '^PATH'

Milestone 4 — Write clean dotfiles

Bash
## ~/.bashrc -- the primary configuration file
## Add these sections in order:
cat > ~/.bashrc << 'DOTFILE'
## --- Environment Variables ------------------------------
export EDITOR="vim"
export PAGER="less"
export LANG="en_US.UTF-8"
export LC_ALL="en_US.UTF-8"
## --- PATH ----------------------------------------------
export PATH="$HOME/.local/bin:$HOME/bin:/usr/local/bin:$PATH"
## --- History Configuration ------------------------------
export HISTSIZE=10000
export HISTFILESIZE=20000
export HISTCONTROL=ignoredups:erasedups
## Share history across all terminal sessions
shopt -s histappend
export PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"
## --- Aliases -------------------------------------------
alias ll='ls -lah'
alias la='ls -la'
alias ..='cd ..'
alias ...='cd ../..'
alias grep='grep --color=auto'
## Kubernetes shortcuts
alias k='kubectl'
alias kgp='kubectl get pods'
alias kgs='kubectl get svc'
alias kd='kubectl describe'
## Docker shortcuts
alias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias dlogs='docker logs -f --tail 100'
## --- Functions -----------------------------------------
## Quick grep through logs
loggrep() {
grep -E "${1}" /var/log/"${2:-app}"/app.log
}
## SSH with port forward to internal service
tunnel() {
local local_port="$1"
local remote_host="$2"
local remote_port="${3:-$1}"
ssh -fN -L "${local_port}:${remote_host}:${remote_port}" bastion
echo "Tunnel open: localhost:${local_port} -> ${remote_host}:${remote_port}"
}
## --- Prompt --------------------------------------------
## Show: user@host:dir (git branch) $
parse_git_branch() {
git branch 2>/dev/null | grep '*' | sed 's/* //'
}
export PS1='\[\e[32m\]\u@\h\[\e[m\]:\[\e[34m\]\w\[\e[m\]$(parse_git_branch=" (\$(parse_git_branch))")\$ '
DOTFILE

Milestone 5 — Per-project environments with direnv

direnv automatically loads and unloads environment variables when you enter and leave a directory. Essential for projects with different Python versions, AWS credentials, or database URLs.

Bash
## Install direnv
sudo apt install direnv
## Hook direnv into bash (add to ~/.bashrc)
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
source ~/.bashrc
## Create .envrc for a project
cd /opt/payment-api
cat > .envrc << 'EOF'
## Payment API development environment
export NODE_ENV=development
export PORT=4000
export DATABASE_URL="postgresql://localhost:5432/payment_dev"
export LOG_LEVEL=debug
## Load secrets from a non-committed file
source_env_if_exists .env.local
EOF
## Allow the .envrc (must explicitly approve)
direnv allow .
## Now when you cd into the directory:
cd /opt/payment-api
## direnv: loading /opt/payment-api/.envrc
## direnv: export +DATABASE_URL +NODE_ENV +PORT ...
echo $NODE_ENV ## development
## When you leave:
cd /home/rahul
## direnv: unloading
echo $NODE_ENV ## (empty -- restored to previous state)
## .gitignore: never commit .env.local
echo ".env.local" >> .gitignore
## Commit .envrc (with non-sensitive values)
git add .envrc

Milestone 6 — System-wide environment for services

Bash
## /etc/environment -- system-wide variables for all processes
## Simple KEY=VALUE format, no shell syntax, no export keyword
sudo tee /etc/environment << 'EOF'
LANG=en_US.UTF-8
LC_ALL=en_US.UTF-8
TZ=Asia/Kolkata
EOF
## /etc/profile.d/ -- shell scripts sourced at login for all users
## Supports shell syntax, functions, and conditional logic
sudo tee /etc/profile.d/devops-tools.sh << 'EOF'
## Add DevOps tools to PATH for all users
if [ -d "/opt/devops-tools/bin" ]; then
export PATH="/opt/devops-tools/bin:$PATH"
fi
EOF
## For systemd services: EnvironmentFile in unit file
sudo tee /etc/payment-api/env << 'EOF'
NODE_ENV=production
PORT=4000
DB_HOST=10.0.2.100
DB_PORT=5432
LOG_LEVEL=info
EOF
## Secure the file (contains potentially sensitive config)
sudo chmod 640 /etc/payment-api/env
sudo chown root:payment-svc /etc/payment-api/env
## Reference in unit file
grep EnvironmentFile /etc/systemd/system/payment-api.service
## EnvironmentFile=/etc/payment-api/env
## Verify the service loaded the variables
sudo systemctl show payment-api --property=Environment
## Or check the running process
sudo strings /proc/$(pgrep -f payment-api | head -1)/environ | grep 'NODE_ENV\|PORT'

Production Best Practices and Common Pitfalls

Scenario Wrong Correct
System-wide PATH Edit /etc/environment Use /etc/profile.d/*.sh
Service environment Hardcode in unit file EnvironmentFile with 640 permissions
Project-specific vars Set globally in ~/.bashrc Use direnv .envrc per project
Secrets in environment echo $API_KEY in scripts Load from secrets manager at runtime
Cron PATH issues Wonder why it fails Set PATH explicitly in every cron script

Quick Reference and Troubleshooting Commands

Task Command
Show all env vars env or printenv
Export variable export VAR=value
Show PATH echo $PATH
Which binary runs which cmd or type -a cmd
Apply dotfile changes source ~/.bashrc
Check service env `sudo cat /proc/PID/environ
direnv allow direnv allow .
direnv status direnv status
Tip

Use type -a commandname instead of which. type -a shows ALL matching binaries in PATH order, reveals if a command is an alias or function rather than a binary, and works in all shells. which only shows the first match and misses shell builtins.

Remember

Changes to ~/.bashrc do not apply to the current session until you run source ~/.bashrc (or . ~/.bashrc). Opening a new terminal also works. Running bash starts a new shell that sources .bashrc, but you are now inside a sub-shell — exit returns you to the original.

Security

Never store secrets in dotfiles committed to Git. ~/.bashrc, ~/.bash_profile, and ~/.profile are often stored in dotfile repositories. Any secret in these files is visible to everyone with access to the repository. Use direnv with a non-committed .env.local file for local secrets.

Common Mistake

Setting variables in ~/.bashrc and expecting them to work in cron jobs or systemd services. ~/.bashrc is only sourced for interactive bash sessions. Cron and systemd have their own separate environments. Set variables in /etc/environment for system-wide persistence or EnvironmentFile in unit files for service-specific config.

Resources

Explore More in Linux Shell Scripting and Automation

All 5 Topics

Frequently Asked Questions

Is Configuring Linux Environment — Variables, PATH, Dotfiles, and Shell Startup free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Configuring Linux Environment — Variables, PATH, Dotfiles, and Shell Startup topic cover?

Master Linux environment variables, PATH management, dotfile configuration, shell startup file ordering, and direnv for consistent production and development environments.