DEV Community

Alan Varghese
Alan Varghese

Posted on

Stop SSH-ing One by One: Building a Parallel Command Executor in Bash

Learn how to build a robust, multi-server SSH command runner using Bash, Docker, and parallel processing.

As developers or system administrators, we've all been there: You need to check the disk space, uptime, or service status on 10 different servers.

The "manual" way is painful:

  1. ssh user@server1 -> df -h -> exit
  2. ssh user@server2 -> df -h -> exit
  3. ...repeat 8 more times. 😫

Sure, tools like Ansible exist, but sometimes you just want a lightweight, zero-dependency script to fire off a quick command and see what's happening right now.

In this post, I'll walk you through how I built a Multi-Server SSH Executor using pure Bash. We'll explore parallel processing, robust file parsing, and how to simulate a server cluster locally using Docker.

🎯 The Goal

We want a script that takes a command (e.g., uptime) and runs it on a list of servers defined in a config file.

Requirements:

  1. Parallel Execution: Use threading (background processes) so checking 10 servers takes as long as the slowest one, not the sum of all of them.
  2. Robust Config Parsing: Handle comments, weird whitespace, and different ports/users.
  3. Local Testing Ground: A way to test this without buying 5 VPS instances (spoiler: we use Docker).

πŸ—οΈ The Architecture

The project consists of three main parts:

  • servers.conf: A simple file defining our target servers.
  • multi_ssh.sh: The brains of the operation.
  • docker-compose.yml: A simulated lab environment with 4 SSH-enabled containers.

1. The Configuration

I wanted a simple format that's easy to read but flexible:
name:hostname:port:username

# Production Servers
web01:192.168.1.10:22:admin
db01:192.168.1.20:22:dbadmin

# Docker Lab (Localhost mapped ports)
web1:localhost:2221:root
web2:localhost:2222:root
Enter fullscreen mode Exit fullscreen mode

2. The Simulation (Docker Lab)

Testing SSH scripts on production servers is... brave. Instead, I used docker-compose to spin up lightweight Ubuntu containers running sshd.

services:
  web1:
    image: rastasheep/ubuntu-sshd:18.04
    ports: ["2221:22"]
  web2:
    image: rastasheep/ubuntu-sshd:18.04
    ports: ["2222:22"]
Enter fullscreen mode Exit fullscreen mode

Now I have "real" servers running on localhost ports 2221, 2222, etc.

⚑ The "Secret Sauce": Parallelism in Bash

The core challenge is running commands simultaneously. In Bash, we do this by putting a command in the background with &.

Here is the simplified logic:

# Loop through servers
for server in "${servers[@]}"; do
    # Run SSH in the background
    ssh $user@$host "$command" > "/tmp/result_$server.txt" &

    # Save the Process ID (PID)
    pids+=($!)
done

# Wait for all background jobs to finish
wait
Enter fullscreen mode Exit fullscreen mode

This simple trick reduces execution time from (N * Timeout) to (Max(Timeout)).

🧠 Lessons Learned & "Gotchas"

Writing the script revealed a few common Bash pitfalls that I had to fix to make it production-ready.

Lesson 1: for loops vs. while read

Initially, I used a for loop to read lines from the config file.
The Trap: If a line has spaces (like a description), for splits it into multiple items.
The Fix: Use a while loop with a custom Internal Field Separator (IFS).

# Robust way to read lines
while IFS=':' read -r name hostname port username || [[ -n "$name" ]]; do
    # Process server...
done < "$config_file"
Enter fullscreen mode Exit fullscreen mode

Note the || [[ -n "$name" ]] partβ€”this ensures we don't skip the last line if the file doesn't end with a newline character!

Lesson 2: Race Conditions & Temp Files

When running parallel jobs, you can't just write to output.txt. Multiple processes will write at the same time, garbling the text.
The Fix: Give each process its own temporary file (e.g., /tmp/ssh_result_web1.txt), let them finish, and then aggregate the results sequentially.

I used mktemp to ensure my temporary files never collided with other running instances of the script.

SERVERS_LIST_TMP=$(mktemp /tmp/ssh_multi_servers.XXXXXX)
Enter fullscreen mode Exit fullscreen mode

Lesson 3: SSH is picky

Running SSH non-interactively requires specific flags to avoid hanging:

  • -o BatchMode=yes: Fail instead of asking for a password.
  • -o ConnectTimeout=X: Don't wait forever if a server is down.
  • -o StrictHostKeyChecking=no: Crucial for automated environments where IPs might change (like Docker containers).

πŸš€ The Result

Running ./multi_ssh.sh "df -h" gives me a beautiful, color-coded summary of disk space across my entire fleet in seconds.

πŸ“₯ Try It Yourself

I've open-sourced this tool along with the setup script that automatically generates SSH keys and configures the Docker containers for you.

Prerequisites:

  • Docker & Docker Compose
  • sshpass (for the initial setup script)

Installation:

git clone https://github.com/alanvarghese-dev/Bash_Scripting/tree/main/ssh_multi_server_executor.git

cd ssh-multi-server-executor
./ssh_install.sh  # Sets up the Docker lab
./multi_ssh.sh "uptime"
Enter fullscreen mode Exit fullscreen mode

Let me know in the comments if you prefer Bash for these tasks or if you stick to heavier tools like Ansible!

Happy scripting! πŸ’»βœ¨

Top comments (0)