Three years ago, I was the developer everyone avoided during code reviews. My scripts worked perfectly on my laptop but exploded spectacularly in production. My pull requests came with disclaimers. My deployments required crossed fingers and prayer circles.
Today, I'm the person they call when production is on fire and nothing else works.
The difference? I stopped writing bash scripts for my machine and started writing them for the real world.
This isn't a story about becoming a bash wizard overnight. It's about the messy, embarrassing, sometimes career-threatening journey from "works for me" to "works everywhere." And how bash scripting became my secret weapon for going from junior developer to the person management trusts with the mission-critical stuff.
🔥 Chapter 1: The "It Works on My Machine" Era
Let me paint you a picture of 2021 me:
#!/bin/bash
# Deploy script (circa 2021 - DO NOT USE)
cd /home/myusername/projects/myapp
git pull
npm install
npm run build
cp -r build/* /var/www/html/
sudo systemctl restart nginx
echo "Deployed!"
This script worked flawlessly on my Ubuntu laptop. In production (CentOS, different paths, different user permissions)? It was like setting off a grenade in a server room.
What was wrong with this?
- Hardcoded paths that only existed on my machine
- No error checking (if git pull failed, we'd deploy broken code)
- Assumed specific OS and software versions
- Required manual setup that I'd forgotten about
- No rollback plan when things went sideways
The wake-up call came during a Friday afternoon deployment. The script failed halfway through, leaving the site in a broken state for 3 hours. My manager asked me to "think about whether I was ready for production deployments."
That hurt. But it also motivated me to get serious about writing bash scripts that actually work.
🎯 Chapter 2: Learning to Think Like Production
The first lesson hit me hard: your development environment is a lie.
Production environments are mean. They have different users, different paths, missing dependencies, restrictive permissions, and they're usually running on different operating systems than your MacBook.
Here's how I learned to write bash that survives contact with reality:
Environment Assumptions Are Your Enemy
# Old me: Assume everything
cd ~/myproject
# New me: Verify everything
get_project_dir() {
# Try multiple possible locations
local possible_dirs=(
"$HOME/projects/myapp"
"/opt/myapp"
"/var/www/myapp"
"$(pwd)"
)
for dir in "${possible_dirs[@]}"; do
if [[ -d "$dir" && -f "$dir/package.json" ]]; then
echo "$dir"
return 0
fi
done
echo "ERROR: Could not find project directory" >&2
echo "Searched: ${possible_dirs[*]}" >&2
return 1
}
PROJECT_DIR=$(get_project_dir) || exit 1
cd "$PROJECT_DIR"
Dependency Hell Is Real
# Old me: Assume tools exist
npm install
# New me: Check everything first
check_dependencies() {
local required_commands=("node" "npm" "git")
local missing=()
for cmd in "${required_commands[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "ERROR: Missing required tools: ${missing[*]}" >&2
echo "Install them and try again." >&2
return 1
fi
# Check versions too
local node_version=$(node --version | cut -d'v' -f2)
local required_node="14.0.0"
if ! version_greater_equal "$node_version" "$required_node"; then
echo "ERROR: Node.js $required_node+ required, found $node_version" >&2
return 1
fi
}
version_greater_equal() {
printf '%s\n%s\n' "$2" "$1" | sort -V -C
}
The Permission Nightmare
# Old me: Hope for the best
cp -r build/* /var/www/html/
# New me: Handle permissions gracefully
safe_deploy() {
local source_dir="$1"
local target_dir="$2"
# Check if we can write to target
if [[ ! -w "$target_dir" ]]; then
echo "No write permission to $target_dir" >&2
# Try with sudo if available
if command -v sudo >/dev/null && sudo -n true 2>/dev/null; then
echo "Using sudo for deployment..."
sudo cp -r "$source_dir"/* "$target_dir"/
else
echo "ERROR: Cannot write to $target_dir and sudo not available" >&2
return 1
fi
else
cp -r "$source_dir"/* "$target_dir"/
fi
}
🛠️ Chapter 3: Building Scripts That Don't Embarrass You
After enough production failures, I developed a framework for writing bash scripts that actually work across environments:
The Production-Ready Template
#!/bin/bash
# Production deployment script v2.0
# Works on: Ubuntu 18+, CentOS 7+, Amazon Linux 2
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Script metadata
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
readonly VERSION="2.0.1"
# Configuration with defaults
PROJECT_NAME="${PROJECT_NAME:-myapp}"
DEPLOY_ENV="${DEPLOY_ENV:-staging}"
NODE_VERSION_MIN="${NODE_VERSION_MIN:-14.0.0}"
BACKUP_RETENTION="${BACKUP_RETENTION:-7}"
# Logging
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$1] $2" >&2
}
info() { log "INFO" "$1"; }
warn() { log "WARN" "$1"; }
error() { log "ERROR" "$1"; }
# Pre-flight checks
validate_environment() {
info "Validating environment..."
# Check OS compatibility
if [[ ! -f /etc/os-release ]]; then
error "Cannot determine OS version"
return 1
fi
local os_id=$(grep '^ID=' /etc/os-release | cut -d'=' -f2 | tr -d '"')
case "$os_id" in
ubuntu|centos|amzn|rhel)
info "OS supported: $os_id"
;;
*)
warn "Untested OS: $os_id (proceeding anyway)"
;;
esac
# Check required commands
local required_commands=("node" "npm" "git" "systemctl")
for cmd in "${required_commands[@]}"; do
if ! command -v "$cmd" >/dev/null; then
error "Required command not found: $cmd"
return 1
fi
done
# Validate Node.js version
local node_version=$(node --version | cut -d'v' -f2)
if ! version_check "$node_version" "$NODE_VERSION_MIN"; then
error "Node.js $NODE_VERSION_MIN+ required, found $node_version"
return 1
fi
info "Environment validation passed"
}
# Version comparison function
version_check() {
printf '%s\n%s\n' "$2" "$1" | sort -V -C
}
# Backup current deployment
create_backup() {
local backup_dir="/opt/backups/${PROJECT_NAME}"
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_path="$backup_dir/backup_$timestamp"
info "Creating backup at $backup_path"
mkdir -p "$backup_dir"
if [[ -d "/var/www/$PROJECT_NAME" ]]; then
cp -r "/var/www/$PROJECT_NAME" "$backup_path"
info "Backup created successfully"
else
warn "No existing deployment found, skipping backup"
fi
# Clean old backups
find "$backup_dir" -name "backup_*" -mtime +$BACKUP_RETENTION -delete
}
# The actual deployment
deploy() {
info "Starting deployment of $PROJECT_NAME to $DEPLOY_ENV"
# Find project directory
local project_dir
project_dir=$(find_project_directory) || {
error "Could not locate project directory"
return 1
}
cd "$project_dir"
# Update code
info "Updating source code..."
git fetch origin
git reset --hard "origin/main"
# Install dependencies
info "Installing dependencies..."
npm ci --only=production
# Build application
info "Building application..."
npm run build
# Deploy with proper permissions
info "Deploying to web directory..."
local web_dir="/var/www/$PROJECT_NAME"
# Ensure web directory exists
sudo mkdir -p "$web_dir"
# Copy files with proper ownership
sudo cp -r build/* "$web_dir"/
sudo chown -R www-data:www-data "$web_dir"
sudo chmod -R 755 "$web_dir"
# Restart services
info "Restarting services..."
sudo systemctl reload nginx
info "Deployment completed successfully"
}
# Rollback function
rollback() {
local backup_dir="/opt/backups/${PROJECT_NAME}"
local latest_backup=$(find "$backup_dir" -name "backup_*" -type d | sort | tail -1)
if [[ -z "$latest_backup" ]]; then
error "No backup found for rollback"
return 1
fi
warn "Rolling back to: $latest_backup"
local web_dir="/var/www/$PROJECT_NAME"
sudo rm -rf "$web_dir"
sudo cp -r "$latest_backup" "$web_dir"
sudo chown -R www-data:www-data "$web_dir"
sudo systemctl reload nginx
info "Rollback completed"
}
# Health check
health_check() {
local app_url="http://localhost"
local max_attempts=30
local attempt=1
info "Performing health check..."
while [[ $attempt -le $max_attempts ]]; do
if curl -sf "$app_url" >/dev/null; then
info "Health check passed"
return 0
fi
warn "Health check attempt $attempt failed, retrying..."
sleep 2
((attempt++))
done
error "Health check failed after $max_attempts attempts"
return 1
}
# Find project directory across different environments
find_project_directory() {
local possible_dirs=(
"$SCRIPT_DIR"
"$HOME/projects/$PROJECT_NAME"
"/opt/$PROJECT_NAME"
"/var/www/$PROJECT_NAME-source"
)
for dir in "${possible_dirs[@]}"; do
if [[ -d "$dir" && -f "$dir/package.json" ]]; then
echo "$dir"
return 0
fi
done
return 1
}
# Usage information
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] COMMAND
Commands:
deploy Deploy the application
rollback Rollback to previous version
health Check application health
validate Validate environment only
Options:
-h, --help Show this help message
-v, --version Show version information
Environment Variables:
PROJECT_NAME Application name (default: myapp)
DEPLOY_ENV Deployment environment (default: staging)
NODE_VERSION_MIN Minimum Node.js version (default: 14.0.0)
Examples:
$SCRIPT_NAME deploy
PROJECT_NAME=myapp DEPLOY_ENV=production $SCRIPT_NAME deploy
$SCRIPT_NAME rollback
EOF
}
# Main execution
main() {
local command="${1:-}"
case "$command" in
deploy)
validate_environment
create_backup
deploy
health_check
;;
rollback)
rollback
health_check
;;
health)
health_check
;;
validate)
validate_environment
;;
-h|--help)
usage
exit 0
;;
-v|--version)
echo "$SCRIPT_NAME version $VERSION"
exit 0
;;
"")
error "No command specified"
usage
exit 1
;;
*)
error "Unknown command: $command"
usage
exit 1
;;
esac
}
# Run main function
main "$@"
📈 Chapter 4: The Transformation
This new approach changed everything. Instead of "works on my machine" scripts, I was writing tools that:
- Worked across different environments without modification
- Failed gracefully with helpful error messages
- Could be safely run by other team members
- Had built-in rollback capabilities
- Included health checks to verify deployments
But more importantly, it changed how my colleagues saw me.
The Career Impact
Before:
- "Can someone else handle this deployment?"
- "Let's test this thoroughly before production"
- "Maybe we should have a backup plan"
After:
- "Can you automate this process too?"
- "Your deployment script never fails"
- "Can you teach the team how to write scripts like this?"
The transformation wasn't just technical - it was professional. When you write bash scripts that other people can trust, you become someone other people can trust.
🚀 Chapter 5: Beyond Scripts - Becoming the Go-To Person
As my bash skills improved, something unexpected happened. I became the person management called for:
- Automation projects that could save the company money
- Emergency response when production systems failed
- Process improvements that other teams needed
- Mentoring junior developers who were making the same mistakes I used to make
Here's what I learned: Good bash scripting isn't just about the code - it's about solving real business problems.
The Scripts That Got Me Promoted
Cost Optimization Script:
# Auto-scaling script that saved $30K/month in cloud costs
scale_based_on_traffic() {
local current_hour=$(date +%H)
local day_of_week=$(date +%u)
# Business hours scaling
if [[ $day_of_week -le 5 && $current_hour -ge 9 && $current_hour -le 17 ]]; then
scale_to 10 # Business hours
elif [[ $day_of_week -le 5 ]]; then
scale_to 3 # After hours weekdays
else
scale_to 1 # Weekends
fi
}
Security Automation Script:
# Automated security scanning that caught vulnerabilities
security_scan() {
# Scan for common security issues
check_sudo_access
check_open_ports
check_ssh_config
check_file_permissions
generate_security_report
}
Data Processing Pipeline:
# ETL pipeline that processed millions of records daily
process_data_pipeline() {
extract_from_database
transform_data_format
validate_data_quality
load_to_warehouse
update_business_dashboards
}
These weren't just scripts - they were solutions to real business problems. And that's what got me noticed.
🎯 Chapter 6: The Lessons That Matter
Looking back, here's what really made the difference in my career:
1. Write for Others, Not Yourself
Your script will be run by someone else, on a different machine, in six months when you're on vacation and production is down.
2. Error Messages Are User Experience
A good error message tells you what went wrong and what to do about it. A great error message prevents the error from happening again.
3. Documentation Is Your Future Self's Best Friend
That clever one-liner you're proud of? You won't remember what it does in three months. Write comments like your job depends on it.
4. Test on Real Systems
Your MacBook is not production. Test on the actual operating systems and environments where your scripts will run.
5. Automate Your Own Annoyances
The tasks that annoy you daily are probably annoying other people too. Solve them, and you become valuable.
6. Think Like a Product Manager
What problem does this script solve? Who will use it? How will they know if it worked? These questions matter more than the code itself.
💡 The Real Secret
Here's what nobody tells you about bash scripting and career growth:
The technical skills are only half the battle.
The other half is:
- Understanding business problems and how to solve them
- Communication skills to explain technical solutions to non-technical people
- Reliability in delivering solutions that actually work
- Initiative in identifying problems before they become crises
The developers who get promoted aren't necessarily the ones who write the most elegant code. They're the ones who write code that makes other people's lives easier.
🚀 Level Up Your Career Through Bash
If you're tired of being the "it works on my machine" developer, if you want to become the person others trust with important problems, if you want to use bash scripting as a career accelerator - I've been exactly where you are.
The journey from junior developer to production hero isn't just about learning syntax. It's about changing how you think about problems, how you approach solutions, and how you deliver value to your organization.
🎓 Bash Scripting for DevOps - Complete Course
This course will teach you:
- Production-ready scripting patterns that work across environments
- Error handling and recovery strategies that prevent disasters
- Automation techniques that solve real business problems
- Security and monitoring practices that protect your organization
- Career development strategies through technical excellence
I also share more career advice, advanced techniques, and real-world case studies on my website: htdevops.top
Don't stay stuck in "it works on my machine" land. Your career - and your teammates - deserve better.
What's your biggest "it works on my machine" story? Share it in the comments - we've all been there, and learning from each other's mistakes is how we all get better! 💪
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.