Every time you open a terminal on RHEL 9, Bash reads one or more startup files before presenting you with a prompt. Which files are read depends on whether the shell is a login shell (started by SSH, a console login, or su -) or an interactive non-login shell (a new terminal window in a GUI, or a subshell). Understanding this distinction is essential for configuring environment variables correctly: variables that need to be available system-wide must be in different files than variables needed only for interactive sessions, and both are different from variables set for scripts running non-interactively via cron or systemd.

This guide covers the complete Bash startup file hierarchy on RHEL 9: /etc/profile, /etc/profile.d/, ~/.bash_profile, ~/.bashrc, ~/.bash_logout, setting environment variables correctly, customising the prompt (PS1), creating aliases, managing PATH safely, and using direnv for per-project environment isolation.

Prerequisites

  • RHEL 9 server with a user account
  • Bash shell (default on RHEL 9)

Step 1 — Understand the Bash Startup File Hierarchy

Login shell startup order:

  1. /etc/profile — system-wide settings, reads all files in /etc/profile.d/
  2. ~/.bash_profile or ~/.bash_login or ~/.profile (first one found)

Interactive non-login shell startup order:

  1. /etc/bashrc — system-wide interactive shell settings
  2. ~/.bashrc — user’s interactive shell settings

The RHEL 9 convention: ~/.bash_profile sources ~/.bashrc, so most user customisations go in ~/.bashrc and are available in both login and non-login interactive sessions.

# Check which startup file is being used
bash --login --norc -c 'echo $BASH_SOURCE'

# Trace which files are sourced on login
bash -x --login 2>&1 | grep "^+" | head -30

Step 2 — Set System-Wide Environment Variables

For environment variables that all users need, create a file in /etc/profile.d/:

vi /etc/profile.d/custom-env.sh
#!/bin/bash
# Custom system-wide environment variables

# Application home directory
export APP_HOME=/opt/myapp

# Java home (if installed system-wide)
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk

# Add to PATH safely (check it is not already there)
case ":$PATH:" in
    *":$APP_HOME/bin:"*) ;;
    *) export PATH="$APP_HOME/bin:$PATH" ;;
esac

Make it executable:

chmod +x /etc/profile.d/custom-env.sh

The change takes effect for all users on next login.

Step 3 — Configure User ~/.bashrc

The ~/.bashrc file is read for every interactive shell. It is the right place for aliases, functions, and interactive-only settings:

vi ~/.bashrc
# Source global bashrc (always include this)
if [ -f /etc/bashrc ]; then
    . /etc/bashrc
fi

# =====================
# Aliases
# =====================
alias ll='ls -alFh --color=auto'
alias la='ls -A'
alias l='ls -CF'
alias grep='grep --color=auto'
alias ..='cd ..'
alias ...='cd ../..'
alias df='df -h'
alias du='du -h'
alias free='free -h'
alias ps='ps auxf'
alias mkdir='mkdir -pv'
alias cp='cp -iv'    # Interactive and verbose
alias mv='mv -iv'
alias rm='rm -Iv --preserve-root'   # Verbose, confirm on >3 files

# Git aliases
alias gs='git status'
alias ga='git add'
alias gc='git commit'
alias gp='git push'
alias gl='git log --oneline --graph --decorate'

# Quick directory shortcuts
alias home='cd ~'
alias logs='cd /var/log'
alias web='cd /var/www/html'

# =====================
# Functions
# =====================
# Create and enter a directory
mkcd() {
    mkdir -p "$1" && cd "$1"
}

# Show disk usage for current directory, sorted
dusort() {
    du -sh ./* 2>/dev/null | sort -h
}

# Extract any archive
extract() {
    case "$1" in
        *.tar.bz2)   tar xjf "$1"   ;;
        *.tar.gz)    tar xzf "$1"   ;;
        *.bz2)       bunzip2 "$1"   ;;
        *.gz)        gunzip "$1"    ;;
        *.tar)       tar xf "$1"    ;;
        *.zip)       unzip "$1"     ;;
        *.7z)        7z x "$1"      ;;
        *)           echo "'$1' cannot be extracted via extract()" ;;
    esac
}

# =====================
# Environment Variables
# =====================
export EDITOR=vim
export VISUAL=vim
export PAGER=less
export LESS='-R --quit-if-one-screen'
export HISTSIZE=10000
export HISTFILESIZE=20000
export HISTCONTROL=ignoredups:erasedups
export HISTTIMEFORMAT="%F %T  "

# =====================
# History settings
# =====================
# Append to history instead of overwriting
shopt -s histappend

# Save and reload history after every command
PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"

Step 4 — Customise the Bash Prompt (PS1)

A well-designed prompt shows the information you need at a glance:

# Add to ~/.bashrc

# Colour variables
RED='[33[0;31m]'
GREEN='[33[0;32m]'
YELLOW='[33[1;33m]'
BLUE='[33[0;34m]'
CYAN='[33[0;36m]'
WHITE='[33[1;37m]'
RESET='[33[0m]'

# Git branch in prompt
git_branch() {
    git symbolic-ref --short HEAD 2>/dev/null | sed 's/^/ (/;s/$/)/'
}

# Show user@host in red if root, green if normal user
if [ "$EUID" -eq 0 ]; then
    USERCOLOR="$RED"
else
    USERCOLOR="$GREEN"
fi

# Format: user@host:/current/dir (git-branch) $
PS1="${USERCOLOR}u@h${RESET}:${BLUE}w${YELLOW}$(git_branch)${RESET}$ "

Step 5 — Configure ~/.bash_profile for Login Sessions

vi ~/.bash_profile
# Source .bashrc for login shells (RHEL convention)
if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

# User-specific environment (not appropriate for .bashrc)
export GPG_TTY=$(tty)

# SSH agent auto-start
if [ -z "$SSH_AUTH_SOCK" ]; then
    eval "$(ssh-agent -s)"
    ssh-add ~/.ssh/id_ed25519 2>/dev/null
fi

Step 6 — Manage PATH Safely

Appending to PATH carelessly can break things. Always check before adding:

# Safe PATH addition function
add_to_path() {
    case ":$PATH:" in
        *":$1:"*) ;;   # already in PATH
        *) export PATH="$1:$PATH" ;;
    esac
}

# Usage
add_to_path "$HOME/.local/bin"
add_to_path "$HOME/go/bin"
add_to_path "/opt/node/bin"

Step 7 — Set Environment Variables for Non-Interactive Use

Variables needed by cron jobs, systemd services, or scripts that do not source ~/.bashrc must be set differently:

# For systemd services: use EnvironmentFile in the unit
# /etc/myapp/env:
APP_ENV=production
DATABASE_URL=postgres://localhost/mydb

# For cron jobs: set at the top of the crontab
EDITOR=nano crontab -e

# At the top of the crontab file:
PATH=/usr/local/bin:/usr/bin:/bin
APP_HOME=/opt/myapp
[email protected]

Step 8 — Apply Changes Without Logging Out

# Reload .bashrc in the current session
source ~/.bashrc
# or
. ~/.bashrc

# Reload .bash_profile
source ~/.bash_profile

Verification Checklist

# Show current environment variables
env | sort

# Show PATH
echo $PATH | tr ':' 'n'

# Show current PS1
echo "$PS1"

# Test an alias
type ll

# Verify history settings
echo $HISTSIZE $HISTFILESIZE $HISTCONTROL

Conclusion

Your Bash environment on RHEL 9 is now configured with a logical file hierarchy: system-wide variables in /etc/profile.d/, user aliases and functions in ~/.bashrc, login-session settings in ~/.bash_profile, a colour prompt with Git branch display, safe PATH management, and unlimited history with timestamps. A well-configured shell environment dramatically improves daily administrative efficiency.

Next steps: How to Use tmux for Terminal Multiplexing on RHEL 9, How to Use vim on RHEL 9, and How to Schedule Automated Tasks with cron on RHEL 9.