DEV Community

Heinan Cabouly
Heinan Cabouly

Posted on

From "It Works on My Machine" to Production Hero: A Bash Journey

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!"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

🛠️ 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 "$@"
Enter fullscreen mode Exit fullscreen mode

📈 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.