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
direnvfor 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:
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 functionsDetailed Step-by-Step Practical Lab
Milestone 1 — Understand shell vs environment variables
## Shell variable (local to current shell)MY_VAR="hello"echo $MY_VAR ## hellobash -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 variablesenvprintenv ## 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 onlyNODE_ENV=staging node server.js## After command, NODE_ENV reverts ## Unset a variableunset MY_VAR ## Make a variable read-onlyreadonly DB_HOST="10.0.2.100"DB_HOST="other" ## Error: readonly variableMilestone 2 — Trace the startup file order
## 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 tracebash --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 systemctlsudo systemctl show payment-api --property=Environmentsudo cat /proc/$(systemctl show payment-api -p MainPID --value)/environ | tr '\0' '\n'Milestone 3 — Manage PATH correctly
## View current PATHecho $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 usercat >> ~/.bashrc << 'EOF' ## Custom toolsexport 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 issuescommand -v node## /usr/local/bin/node## If empty: node is not in PATH ## Check exact PATH for a running servicesudo cat /proc/$(pgrep payment-api | head -1)/environ | tr '\0' '\n' | grep '^PATH'Milestone 4 — Write clean dotfiles
## ~/.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=10000export HISTFILESIZE=20000export HISTCONTROL=ignoredups:erasedups## Share history across all terminal sessionsshopt -s histappendexport 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 shortcutsalias k='kubectl'alias kgp='kubectl get pods'alias kgs='kubectl get svc'alias kd='kubectl describe' ## Docker shortcutsalias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'alias dlogs='docker logs -f --tail 100' ## --- Functions -----------------------------------------## Quick grep through logsloggrep() { grep -E "${1}" /var/log/"${2:-app}"/app.log} ## SSH with port forward to internal servicetunnel() { 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))")\$ ' DOTFILEMilestone 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.
## Install direnvsudo apt install direnv ## Hook direnv into bash (add to ~/.bashrc)echo 'eval "$(direnv hook bash)"' >> ~/.bashrcsource ~/.bashrc ## Create .envrc for a projectcd /opt/payment-apicat > .envrc << 'EOF'## Payment API development environmentexport NODE_ENV=developmentexport PORT=4000export DATABASE_URL="postgresql://localhost:5432/payment_dev"export LOG_LEVEL=debug ## Load secrets from a non-committed filesource_env_if_exists .env.localEOF ## 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: unloadingecho $NODE_ENV ## (empty -- restored to previous state) ## .gitignore: never commit .env.localecho ".env.local" >> .gitignore## Commit .envrc (with non-sensitive values)git add .envrcMilestone 6 — System-wide environment for services
## /etc/environment -- system-wide variables for all processes## Simple KEY=VALUE format, no shell syntax, no export keywordsudo tee /etc/environment << 'EOF'LANG=en_US.UTF-8LC_ALL=en_US.UTF-8TZ=Asia/KolkataEOF ## /etc/profile.d/ -- shell scripts sourced at login for all users## Supports shell syntax, functions, and conditional logicsudo tee /etc/profile.d/devops-tools.sh << 'EOF'## Add DevOps tools to PATH for all usersif [ -d "/opt/devops-tools/bin" ]; then export PATH="/opt/devops-tools/bin:$PATH"fiEOF ## For systemd services: EnvironmentFile in unit filesudo tee /etc/payment-api/env << 'EOF'NODE_ENV=productionPORT=4000DB_HOST=10.0.2.100DB_PORT=5432LOG_LEVEL=infoEOF ## Secure the file (contains potentially sensitive config)sudo chmod 640 /etc/payment-api/envsudo chown root:payment-svc /etc/payment-api/env ## Reference in unit filegrep EnvironmentFile /etc/systemd/system/payment-api.service## EnvironmentFile=/etc/payment-api/env ## Verify the service loaded the variablessudo systemctl show payment-api --property=Environment## Or check the running processsudo 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 |
PLACEMENT PRO TIP**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 THIS**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.
COMMON MISTAKE / WARNING**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 / WARNING**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.