Update (December 2025): The original script had a critical flaw in VIP detection that could cause both servers to incorrectly identify as PRIMARY. This has been corrected with a more reliable detection method. See the "What Was Fixed" section below for details.
When managing a high availability HAProxy cluster, one of the most common moments of confusion happens right after you SSH into a server: "Wait, am I on the primary or secondary node?"
If you've ever found yourself frantically checking process lists or network interfaces just to figure out which HAProxy server you're on, this smart detection script will solve that problem forever.
The Problem: Which Server Am I On?
In a typical HA setup, you have multiple HAProxy servers where roles can change during failover events. When troubleshooting issues or performing maintenance, you need to know immediately:
- Is this the active server handling traffic?
- Is this a standby server ready for failover?
- What's the server's current network configuration?
Making changes to the wrong node can cause service disruptions and manually checking server status wastes precious time during incidents.
The Solution: Intelligent Auto-Detection
Instead of manually checking server status every time you SSH in, let's create a script that automatically detects and displays the server's current role based on its actual network activity.
Creating the Detection Script
Create the script that will run every time someone logs in:
sudo nano /etc/profile.d/haproxy-status.sh
Here's the corrected and reliable detection script:
#!/bin/bash
# Configuration - SET YOUR ACTUAL VIP HERE
VIP="192.168.1.100" # Change this to your actual Virtual IP
# Function to detect HAProxy role using multiple methods
detect_haproxy_role() {
local hostname=$(hostname)
local ip_addr=$(hostname -I | awk '{print $1}')
local role="UNKNOWN"
local status_icon="❓"
local detection_method=""
echo "🔍 Detecting HAProxy role for $hostname ($ip_addr)..."
echo " Looking for VIP: $VIP"
echo ""
# Method 1: Check if this server has the VIP bound (MOST RELIABLE)
if command -v ip >/dev/null 2>&1; then
if ip addr show | grep -q "inet $VIP/"; then
role="PRIMARY"
status_icon="🟢"
detection_method="Virtual IP $VIP is bound to this server"
else
role="SECONDARY"
status_icon="🟡"
detection_method="Virtual IP $VIP is NOT on this server"
fi
fi
# Method 2: Check keepalived current state (more reliable than old method)
if [[ "$role" == "UNKNOWN" ]] && [[ -f /var/run/keepalived.pid ]]; then
# Check keepalived state from recent logs (extended window)
if journalctl -u keepalived --since "1 hour ago" -q | tail -20 | grep -q "Entering MASTER STATE" 2>/dev/null; then
if ! journalctl -u keepalived --since "1 hour ago" -q | tail -20 | grep -q "Entering BACKUP STATE" 2>/dev/null; then
role="PRIMARY"
status_icon="🟢"
detection_method="keepalived in MASTER state"
fi
elif journalctl -u keepalived --since "1 hour ago" -q | tail -20 | grep -q "Entering BACKUP STATE" 2>/dev/null; then
role="SECONDARY"
status_icon="🟡"
detection_method="keepalived in BACKUP state"
fi
fi
# Method 3: Check if VIP responds from this server
if [[ "$role" == "UNKNOWN" ]] && [[ -n "$VIP" ]]; then
# Check if we can see the VIP in our ARP table as local
if ip neigh show | grep -q "$VIP.*lladdr"; then
role="SECONDARY"
status_icon="🟡"
detection_method="VIP $VIP visible but not local"
fi
fi
# Fallback: Check process existence but default to SECONDARY for safety
if [[ "$role" == "UNKNOWN" ]]; then
if pgrep haproxy >/dev/null; then
role="SECONDARY"
status_icon="⚠️"
detection_method="Unable to determine - defaulting to SECONDARY (safe mode)"
else
role="UNKNOWN"
status_icon="❌"
detection_method="HAProxy not running"
fi
fi
# Display results
echo "$status_icon HAProxy $role - $hostname ($ip_addr)"
echo " Detection: $detection_method"
if [[ "$role" == "PRIMARY" ]]; then
echo " Status: ACTIVE - Handling live traffic via VIP"
echo " Role: Load balancer primary node"
# Show connection details
local https_conns=$(netstat -an 2>/dev/null | grep :443 | grep ESTABLISHED | wc -l)
local http_conns=$(netstat -an 2>/dev/null | grep :80 | grep ESTABLISHED | wc -l)
echo " Active connections: HTTPS=$https_conns, HTTP=$http_conns"
elif [[ "$role" == "SECONDARY" ]]; then
echo " Status: STANDBY - Ready for failover"
echo " Role: Load balancer backup node"
echo " Info: VIP is on the primary server"
else
echo " Status: UNKNOWN - Check configuration"
echo " Role: Unable to determine"
fi
# Show additional diagnostics
echo ""
echo "📊 Additional Information:"
# Show all IPs on this server
echo " Server IPs:"
ip addr show | grep "inet " | grep -v "127.0.0.1" | awk '{print " " $2}'
# HAProxy status
if pgrep haproxy >/dev/null; then
echo " HAProxy Process: ✅ Running (PID: $(pgrep haproxy | tr '\n' ' '))"
else
echo " HAProxy Process: ❌ Not running"
fi
# Keepalived status
if command -v systemctl >/dev/null 2>&1 && systemctl is-active keepalived >/dev/null 2>&1; then
echo " Keepalived: ✅ Active"
# Show priority if possible
if [[ -f /etc/keepalived/keepalived.conf ]]; then
local priority=$(grep "priority" /etc/keepalived/keepalived.conf | head -1 | awk '{print $2}')
if [[ -n "$priority" ]]; then
echo " Keepalived Priority: $priority"
fi
fi
elif command -v systemctl >/dev/null 2>&1; then
echo " Keepalived: ❌ Not active"
fi
# Port bindings
local bound_ports=$(netstat -tlnp 2>/dev/null | grep haproxy | awk '{print $4}' | cut -d: -f2 | sort -n | uniq | tr '\n' ' ')
if [[ -n "$bound_ports" ]]; then
echo " Bound Ports: $bound_ports"
fi
# Management links
echo ""
echo "🔧 Management:"
echo " Stats Dashboard: https://$ip_addr:8404/"
if [[ "$role" == "PRIMARY" ]]; then
echo " VIP Stats: https://$VIP:8404/"
fi
if [[ -S /run/haproxy/admin.sock ]]; then
echo " Admin Socket: ✅ /run/haproxy/admin.sock"
else
echo " Admin Socket: ❌ Not available"
fi
echo ""
}
# Only run for interactive SSH sessions
if [[ $- == *i* ]] && [[ -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then
detect_haproxy_role
fi
Make the Script Executable
sudo chmod +x /etc/profile.d/haproxy-status.sh
What Was Fixed: Critical Detection Issues
The original script had several flaws that could cause both servers to incorrectly identify as PRIMARY:
Problem 1: Loose VIP Detection
Original code:
# Look for secondary IPs that might be VIPs
for vip in $secondary_ips; do
if [[ "$vip" != "$primary_ip" ]]; then
vip_detected=true
role="PRIMARY"
Issue: This detected ANY secondary IP as a VIP, not the actual Virtual IP. If both servers had secondary IPs for management or other purposes, both would identify as PRIMARY.
Fix:
# Configuration - SET YOUR ACTUAL VIP HERE
VIP="192.168.1.100"
# Check if this server has the VIP bound (MOST RELIABLE)
if ip addr show | grep -q "inet $VIP/"; then
role="PRIMARY"
Problem 2: Short Time Window for Keepalived
Original code:
if journalctl -u keepalived --since "5 minutes ago" -q | grep -q "Transition to MASTER STATE"
Issue: Only checked the last 5 minutes. If no state transition occurred recently, the detection would fail to identify the current state.
Fix:
# Extended to 1 hour and check most recent state
if journalctl -u keepalived --since "1 hour ago" -q | tail -20 | grep -q "Entering MASTER STATE"
Problem 3: Arbitrary Traffic Thresholds
Original code:
if [[ $total_connections -gt 5 ]]; then
role="PRIMARY"
Issue: Used arbitrary connection counts. In low-traffic scenarios or during off-peak hours, both servers could fall below the threshold, or the secondary could have residual connections.
Fix: Removed reliance on traffic patterns as primary detection method. VIP presence is now the definitive indicator.
Problem 4: CPU-Based Detection
Original code:
if (( $(echo "$haproxy_cpu > 1" | bc -l) )); then
role="PRIMARY"
Issue: CPU usage varies with load and isn't a reliable indicator of role. Both servers could have low CPU during quiet periods.
Fix: Moved CPU checking to informational only, not used for role determination.

How the Corrected Detection Works
The new script uses a hierarchical detection method with the most reliable techniques first:
1. Virtual IP Detection (Primary Method)
if ip addr show | grep -q "inet $VIP/"; then
role="PRIMARY"
Why it works: The VIP is managed by keepalived and only exists on the active node. This is the most definitive way to determine role.
Configuration Required: You must set your actual VIP at the top of the script:
VIP="192.168.1.100" # Change to your actual VIP
2. Keepalived State Check (Secondary Method)
Checks keepalived logs over a longer time window (1 hour instead of 5 minutes) and examines the most recent state transitions.
3. Safety Fallback
If the script cannot determine the role definitively, it defaults to SECONDARY. This is the safe choice because:
- Prevents accidentally treating a standby server as primary
- Encourages verification before making changes
- Better to be cautious than accidentally disruptive
What You'll See When Logging In
On the Primary Server:
🔍 Detecting HAProxy role for haproxy01 (192.168.1.10)...
Looking for VIP: 192.168.1.100
🟢 HAProxy PRIMARY - haproxy01 (192.168.1.10)
Detection: Virtual IP 192.168.1.100 is bound to this server
Status: ACTIVE - Handling live traffic via VIP
Role: Load balancer primary node
Active connections: HTTPS=47, HTTP=12
📊 Additional Information:
Server IPs:
192.168.1.10/24
192.168.1.100/24
HAProxy Process: ✅ Running (PID: 1234 1235)
Keepalived: ✅ Active
Keepalived Priority: 100
Bound Ports: 80 443 8404
🔧 Management:
Stats Dashboard: https://192.168.1.10:8404/
VIP Stats: https://192.168.1.100:8404/
Admin Socket: ✅ /run/haproxy/admin.sock
On the Secondary Server:
🔍 Detecting HAProxy role for haproxy02 (192.168.1.11)...
Looking for VIP: 192.168.1.100
🟡 HAProxy SECONDARY - haproxy02 (192.168.1.11)
Detection: Virtual IP 192.168.1.100 is NOT on this server
Status: STANDBY - Ready for failover
Role: Load balancer backup node
Info: VIP is on the primary server
📊 Additional Information:
Server IPs:
192.168.1.11/24
HAProxy Process: ✅ Running (PID: 1234 1235)
Keepalived: ✅ Active
Keepalived Priority: 90
Bound Ports: 80 443 8404
🔧 Management:
Stats Dashboard: https://192.168.1.11:8404/
Admin Socket: ✅ /run/haproxy/admin.sock
Configuration Steps
1. Identify Your VIP
First, find your Virtual IP address. Check your keepalived configuration:
grep "virtual_ipaddress" /etc/keepalived/keepalived.conf -A 3
You should see something like:
virtual_ipaddress {
192.168.1.100/24
}
2. Update the Script
Edit the script and set your VIP at the top:
sudo nano /etc/profile.d/haproxy-status.sh
Change this line:
VIP="192.168.1.100" # Change to YOUR actual VIP
3. Deploy to All Nodes
Copy the script to all HAProxy servers in your cluster:
# From your workstation
for host in haproxy01 haproxy02; do
scp /etc/profile.d/haproxy-status.sh root@$host:/etc/profile.d/
ssh root@$host chmod +x /etc/profile.d/haproxy-status.sh
done
4. Test the Detection
SSH into each server and verify the role is correctly detected:
ssh haproxy01 # Should show PRIMARY with VIP
ssh haproxy02 # Should show SECONDARY without VIP
Verifying Correct Operation
Manual VIP Check
On each server, verify which one has the VIP:
# This should only return results on the PRIMARY
ip addr show | grep "192.168.1.100"
Primary server output:
inet 192.168.1.100/24 scope global secondary eth0
Secondary server output:
(no output - VIP not present)
Keepalived State Check
Verify keepalived knows which server is master:
# Check recent state transitions
journalctl -u keepalived --since "1 hour ago" | grep -E "MASTER|BACKUP"
Test Failover Detection
Stop keepalived on the primary to trigger failover:
# On primary server
sudo systemctl stop keepalived
Then SSH into both servers:
- The old primary should now show as SECONDARY (no VIP)
- The old secondary should now show as PRIMARY (has VIP)
Start keepalived again to restore normal operation:
sudo systemctl start keepalived
Benefits in Daily Operations
Immediate Context
- Zero confusion about which server you're on
- Instant awareness of the server's role in your cluster
- Quick access to monitoring and admin tools
- VIP visibility shows exactly which server is handling traffic
Mistake Prevention
- Visual warnings when you're on the active production server
- Clear identification prevents configuration errors
- Role awareness helps you make appropriate changes
- Safe defaults when detection is uncertain
Faster Troubleshooting
- No time wasted checking server status manually
- Immediate connection count shows current load
- Direct links to stats and admin interfaces
- Shows all server IPs for quick reference
Troubleshooting
Both Servers Show as PRIMARY
This was the original bug. If you still see this after updating:
- Verify VIP is set correctly in the script
Ensure only one keepalived is in MASTER state:
journalctl -u keepalived --since "10 minutes ago" | grep STATE
Check which server actually has the VIP:
ip addr show | grep "your.vip.address"
Both Servers Show as SECONDARY
This could indicate:
- VIP isn't configured in the script
- Keepalived isn't running on either server
- Split-brain scenario - both nodes think they're backup
Check keepalived status:
systemctl status keepalived
ip addr show | grep "your.vip.address"
Script Not Running on Login
- Check file permissions:
ls -la /etc/profile.d/haproxy-status.sh - Verify bash syntax:
bash -n /etc/profile.d/haproxy-status.sh - Test manually:
source /etc/profile.d/haproxy-status.sh
Incorrect Role Detection
If the role seems wrong:
Verify keepalived configuration:
cat /etc/keepalived/keepalived.conf | grep -A 10 "vrrp_instance"
Check keepalived logs:
journalctl -u keepalived -n 50
Manually verify VIP location:
# Run on both servers
ip addr show | grep -A 2 "inet.*scope global"
Advanced Enhancements
Add Cluster Health Information
Extend the script to show overall cluster status:
# Add this near the end of detect_haproxy_role function
echo " Cluster Health:"
# Check if HAProxy service is running
if systemctl is-active --quiet haproxy; then
echo " HAProxy Service: ✅ Running"
else
echo " HAProxy Service: ❌ Stopped"
fi
# Show backend health summary
if command -v socat >/dev/null 2>&1 && [[ -S /run/haproxy/admin.sock ]]; then
local backend_count=$(echo 'show stat' | socat stdio /run/haproxy/admin.sock 2>/dev/null | grep -c ',UP,' || echo "0")
local total_backends=$(echo 'show stat' | socat stdio /run/haproxy/admin.sock 2>/dev/null | grep -c 'BACKEND' || echo "0")
echo " Healthy Backends: $backend_count/$total_backends"
fi
Monitor Both Nodes from Primary
Add a check to show the peer's status when on the primary:
# Add this for PRIMARY role only
if [[ "$role" == "PRIMARY" ]] && [[ -n "$SECONDARY_IP" ]]; then
echo ""
echo " Peer Status:"
if ping -c 1 -W 1 "$SECONDARY_IP" >/dev/null 2>&1; then
echo " Secondary ($SECONDARY_IP): ✅ Reachable"
else
echo " Secondary ($SECONDARY_IP): ❌ Unreachable"
fi
fi
Integration with Monitoring
Send role information to your monitoring system:
# Add at the end of detect_haproxy_role
if command -v curl >/dev/null 2>&1; then
curl -s -X POST "https://your-monitoring-system/api/metrics" \
-H "Content-Type: application/json" \
-d "{\"metric\":\"haproxy_role\",\"server\":\"$hostname\",\"role\":\"$role\",\"vip\":\"$VIP\"}" \
>/dev/null 2>&1 || true
fi
Security Considerations
- Minimal information exposure: Only shows operational details
- No sensitive data: Avoids displaying passwords or private keys
- Interactive sessions only: Doesn't run for automated processes
- Local detection: Uses only local system information
- Safe defaults: Assumes SECONDARY when uncertain
Conclusion
This corrected role detection script eliminates the confusion that plagued the original version. By relying on definitive indicators (VIP presence) rather than circumstantial evidence (traffic patterns, CPU usage), you get:
- Accurate detection of server roles
- Reliable failover awareness
- Immediate situational awareness upon login
- Prevention of configuration mistakes
- Time saved during troubleshooting
The script is lightweight, fast and adapts to role changes automatically. Whether you're managing a simple active/standby pair or a complex multi-node cluster, this corrected version will make your daily operations smoother and safer.
Set it up once and never wonder "which server am I on?" again and this time, you'll get the right answer!
Changelog
December 2025 Update:
- Fixed VIP detection to check for specific VIP instead of any secondary IP
- Extended keepalived state check window from 5 minutes to 1 hour
- Removed unreliable traffic-based and CPU-based detection methods
- Added VIP configuration requirement at top of script
- Changed fallback behavior to default to SECONDARY for safety
- Added display of all server IPs for verification
- Added keepalived priority display when available
- Improved error handling and detection reliability