Manage Multiple Servers at Once: Inventory, SSH and Workspaces
Manage multiple servers at once with one inventory file, consistent names, parallel-ssh, Ansible ad-hoc, tmux sync panes and saved workspaces for 10+ hosts.
To manage multiple servers at once, keep one inventory file that names every host and groups it by environment and role. Make those names your SSH aliases. Then pick the tool by job: parallel-ssh for read-only checks across the fleet, Ansible ad-hoc commands for changes, tmux synchronized panes for interactive typing, and a saved DockLab workspace for the two to four servers you actually work on side by side.
Quick answer
- Name hosts
env-role-n(prod-web-1,staging-db-1) and use those exact names as~/.ssh/configaliases. - Keep one inventory file (Ansible YAML works well) with groups like
prod,staging,web,dband one per client. - For read-only checks on many hosts, run
parallel-ssh -h hosts.txt -i -t 30 'uptime'. - For changes, use
ansible <group> -m <module> --become, try-C(check mode) first, and narrow the scope with--limit. - For typing into several shells at once, use tmux with
synchronize-panes on, and only for a handful of hosts. - For daily interactive work, save one workspace per environment or client so the terminal, files and editor come back in one click.
Four kinds of "at once" and the tool for each
"Managing multiple servers at once" covers four different jobs. Most frustration comes from using one tool for all four. We use this split:
| Job | Tool we use | Good at | Weak at |
|---|---|---|---|
| Same read-only command on 10 to 100 hosts | parallel-ssh (package pssh) | Fast, simple, per-host output files, useful exit codes | No idempotency, no templating, sudo prompts break it |
| Changes that must be repeatable | Ansible ad-hoc (later playbooks) | Groups, patterns, check mode, modules that only change what differs | Needs Python on targets, slower than raw ssh, more to learn |
| Typing interactively into several shells | tmux synchronize-panes or ClusterSSH (cssh) | Watch every host react live | Error-prone past 4 to 6 hosts, prompts drift out of sync |
| Working on a few hosts in depth | Saved workspaces (DockLab, or tmux sessions) | Terminal, files and editor per host, restored quickly | Not a fan-out tool |
The rest of this guide sets up the shared foundation (names and inventory), then covers each tool with commands tested on Ubuntu 24.04 LTS.
Step zero: names that work everywhere
Past ten hosts, the name of a server is your interface to it. A good name is short, unambiguous and predictable enough that wildcard patterns can match it. We use env-role-n, and agencies can add a client prefix: acme-prod-web-1.
The important rule: use the same string as the inventory hostname, the SSH alias and the label in your GUI client. Then ssh prod-web-1, ansible prod-web-1 -m ping, a parallel-ssh host list and your saved workspace all point at the same machine. If the name and the real address differ, map them in ~/.ssh/config with HostName, not in each tool separately.
We already published a full ~/.ssh/config for a fleet this size, with Host prod-* patterns, ProxyJump and ControlMaster, in SSH Config File Examples. This guide assumes something similar is in place. parallel-ssh, Ansible's default ssh connection and tmux panes running ssh all call the OpenSSH client, so they all read that file.
One inventory file as the source of truth
A list of hosts in someone's head, a spreadsheet and three GUI clients is the usual starting point. Replace it with one file in a git repo. Even if you never write a playbook, an Ansible YAML inventory is a good format: it supports groups, nested groups, numeric ranges and per-host notes, and tools can query it.
# ~/fleet/hosts.yml
all:
children:
prod:
children:
prod_web:
hosts:
prod-web-[1:4]:
prod_db:
hosts:
prod-db-1:
notes: "Postgres primary, backups at 02:00"
prod-db-2:
notes: "Streaming replica"
staging:
hosts:
staging-web-1:
staging-db-1:
acme:
hosts:
acme-web:
notes: "Client server, ubuntu user, ask before reboot"
web:
hosts:
prod-web-[1:4]:
staging-web-1:
A few things this layout gets right:
- Groups by what, where and when. The Ansible docs suggest grouping by role, location and lifecycle stage so you avoid "testing on production resources". A host can sit in several groups:
prod-web-2is inprod_web,prodandweb. - Ranges keep it short.
prod-web-[1:4]expands to four hosts, inclusive. - No addresses, users or keys here. Those live in
~/.ssh/config. The inventory only says which machines exist and how they group. - Free-form notes. Arbitrary host variables like
notesare ignored by modules but show up inansible-inventory, which makes the file double as documentation.
Point Ansible at it once and check how the groups resolve:
sudo apt install ansible # Ubuntu 24.04 ships ansible 9.x / ansible-core 2.16
export ANSIBLE_INVENTORY=~/fleet/hosts.yml
ansible-inventory --graph
ansible 'prod:&web' --list-hosts
Ansible patterns are the reason the inventory is worth the effort: prod:&web is the intersection (production web servers), web:!staging-web-1 is an exclusion, and prod_web:prod_db is a union. Use single quotes so bash does not expand the !.
Read-only checks across the fleet with parallel-ssh
For questions like "which hosts need a reboot" or "is the nginx config the same everywhere", plain parallel ssh is the fastest tool. On Ubuntu the package is pssh, but the command is parallel-ssh.
Generate host lists from the inventory so you never maintain two lists:
sudo apt install pssh
mkdir -p ~/fleet/lists
ansible prod_web --list-hosts | tail -n +2 | tr -d ' ' > ~/fleet/lists/prod_web.txt
ansible prod --list-hosts | tail -n +2 | tr -d ' ' > ~/fleet/lists/prod.txt
Then run commands:
# Who needs a reboot? -i prints each host's output inline, -t caps each host at 30s
parallel-ssh -h ~/fleet/lists/prod.txt -i -t 30 -p 10 \
'test -f /var/run/reboot-required && echo REBOOT || echo ok'
# Config drift: hash the same file everywhere, write one output file per host
parallel-ssh -h ~/fleet/lists/prod_web.txt -t 30 -o /tmp/nginx-hash \
'sha256sum /etc/nginx/nginx.conf'
cat /tmp/nginx-hash/* | sort | uniq -c
If uniq -c prints one line with a count of 4, all four web servers match. Two lines means one host has drifted, and grep -l on the output directory tells you which one.
Read the exit status. The man page documents distinct codes: 0 means everything succeeded, 3 means a host timed out or was killed, 4 means at least one ssh connection itself failed (status 255), and 5 means ssh worked but a remote command returned non-zero. In scripts, 4 and 5 are different problems: one is your network or keys, the other is the server.
Changes across many hosts with Ansible ad-hoc commands
Once a command changes state (restart a service, install a package, edit a file), we switch to Ansible even for one-off jobs. Modules check the current state first, so running twice is safe, and check mode shows what would change. The ad-hoc command guide covers the syntax. These are the commands we use most:
# Can Ansible reach and run Python on every host?
ansible all -m ansible.builtin.ping
# Disk usage on production, 10 hosts at a time (default is 5 forks)
ansible prod -m ansible.builtin.shell -a 'df -h / | tail -1' -f 10
# Reload nginx on staging web servers only
ansible 'web:&staging' -m ansible.builtin.service -a 'name=nginx state=reloaded' --become -K
# Dry run: what would change if we pushed this file?
ansible prod_web -m ansible.builtin.copy \
-a 'src=./motd dest=/etc/motd owner=root mode=0644' --become -K -C --diff
# Same command, but skip the replica
ansible prod -m ansible.builtin.apt -a 'name=htop state=present' --become -K --limit 'prod:!prod-db-2'
Two details that bite people. The default command module does not support pipes or redirects, which is why the df | tail example uses ansible.builtin.shell. And modules need Python on the target. Ubuntu servers have it, but minimal images and appliances may not, and there ansible.builtin.raw is the fallback.
When you catch yourself running the same three ad-hoc commands every week, that is the moment to turn them into a playbook. Until then, ad-hoc is enough.
Typing into many shells at once: tmux synchronize-panes
Sometimes you want to watch every host respond live: following the same log on four web servers, or stepping through an upgrade that asks questions. tmux handles this with the synchronize-panes option, which the tmux man page describes as duplicating input "to all other panes in the same window where this option is also on".
Save a small launcher script
#!/usr/bin/env bash # ~/bin/fleet-tmux: one tiled pane per host, input synchronized set -euo pipefail session="fleet-$$" tmux new-session -d -s "$session" "ssh $1" shift for host in "$@"; do tmux split-window -t "$session" "ssh $host" tmux select-layout -t "$session" tiled done tmux set-window-option -t "$session" synchronize-panes on tmux attach -t "$session"Running
select-layout tiledafter every split matters. Without it, tmux runs out of room and fails with "no space for new pane" after a few splits.Open a group from the inventory
chmod +x ~/bin/fleet-tmux fleet-tmux $(cat ~/fleet/lists/prod_web.txt)Toggle sync when you need one host alone
Inside tmux, press the prefix (
Ctrl-bby default), type:setw synchronize-panes off, do the per-host work, then turn it back on. Binding this to a key is worth it if you use it often.
ClusterSSH does the same in a GUI: sudo apt install clusterssh, then cssh prod-web-1 prod-web-2 opens one terminal window per host plus a control window that sends input to all of them. It needs an X11 display, so it fits best on a Linux desktop.
Our rule for both: no more than four to six hosts, and never on production without a dry run first. Prompts drift apart (one host asks a dpkg question, the others do not), and synchronized keystrokes keep landing anyway. For anything larger, go back to parallel-ssh or Ansible.
Working on a few hosts in depth: saved workspaces
Fan-out tools solve the "same command everywhere" problem. Most of the day, though, the work looks different: you are debugging prod-web-2 while tailing the database log on prod-db-1, with a config file open and an SFTP view of the deploy directory. With 10+ hosts, rebuilding that context every morning is the real time sink.
On the terminal side, a named tmux session per task (tmux new -s acme-incident) on the server keeps processes alive across disconnects. That is the right tool for long-running work, and no desktop client replaces it.
On the desktop side, this is where DockLab saves us time. DockLab puts the SSH terminal, SFTP browser and a Monaco-based remote editor in one window. A saved workspace restores the tabs and up to four split panes. We keep one workspace per environment or client ("prod web + db", "acme"), named to match the inventory groups. With optional encrypted sync, saved layouts follow you to another machine (AI conversation history does not). Two limits to be honest about: saving a layout does not keep remote processes running, so still use tmux on the server for that, and DockLab is not a fan-out tool. When we need to hit 20 hosts, we run parallel-ssh or ansible from a DockLab terminal tab like anywhere else. Credentials and imported keys sit in a local AES-256-GCM encrypted store, and host key pinning flags a server whose identity changed, which matters more as the fleet grows.
If you need mobile access, Termius has iOS and Android apps and DockLab does not. If you are on Windows and need many protocols beyond SSH, MobaXterm covers far more. Our comparison page goes through those trade-offs. The free plan has host limits, while Pro and Team list unlimited hosts and encrypted sync, so check pricing if you are past ten servers.
Failure modes that only show up at 10+ hosts
- Bastion drops connections during parallel runs. Every
ProxyJumpopens a new connection to the bastion. sshd'sMaxStartupsdefaults to10:30:100, so once more than 10 connections are still unauthenticated, it starts refusing new ones at random. Withparallel-ssh -p 32through one bastion, some hosts fail with connection reset errors. Fix it by lowering-pto 10 or-fin Ansible, or by enablingControlMasterfor the bastion so jumps share one connection. - Host key prompts stall batch tools. A new server's "are you sure you want to continue connecting" prompt blocks parallel-ssh and Ansible. Use
StrictHostKeyChecking accept-newfor your fleet's patterns, or primeknown_hostsafter verifying fingerprints. - "Too many authentication failures". An agent holding many keys burns through
MaxAuthTriesbefore the right one is offered. Pin keys per group withIdentityFileandIdentitiesOnly yes. - Stale ControlMaster sockets after laptop sleep. New sessions hang silently.
ssh -O exit hostnameclears it. - The inventory drifts from reality. Decommissioned hosts time out on every run. Treat the inventory like code: a server is not "removed" until the line is deleted in a commit.
- Partial success looks like success. Nine of ten hosts updated and one timed out. Always read the summary (Ansible's recap, parallel-ssh's exit code) before moving on.
Frequently Asked Questions
What is the best way to run the same command on multiple Linux servers?
For read-only commands, parallel-ssh from the pssh package is the simplest choice: give it a host list and a command, and it runs over SSH in parallel with per-host output. For anything that changes state, use an Ansible ad-hoc command, because modules only apply changes that are needed and support check mode. Both read your ~/.ssh/config, so aliases and jump hosts keep working.
Is tmux synchronize-panes safe for production servers?
It is safe for watching output, such as tailing logs, and risky for changes. Keystrokes go to every pane even when one host shows a different prompt or an unexpected question. Limit it to a handful of hosts, and do real changes through Ansible with -C first.
Do I need Ansible if I only have 10 to 20 servers?
You do not need playbooks, but an Ansible inventory file and ad-hoc commands pay off quickly at that size. The inventory gives you named groups and patterns like prod:&web, and ad-hoc commands give you safe, repeatable changes without writing any YAML beyond the inventory itself. Installing it on Ubuntu is a single apt install ansible.
How should I name servers when I manage many of them?
Use a predictable scheme like env-role-n (prod-web-1, staging-db-1), with a client prefix if you manage servers for several clients. Use the exact same name as the SSH alias, the inventory hostname and the label in any GUI client. That consistency is what lets wildcard patterns, host lists and saved workspaces all refer to the same machine.
Can a GUI SSH client replace parallel-ssh or Ansible?
Usually not, and it should not try to. GUI clients like DockLab are best for interactive work on a few hosts at a time, with terminal, files and editor side by side. Fan-out commands across dozens of hosts belong to parallel-ssh or Ansible, which you can run from any terminal tab, including one inside the GUI client.
SSH, SFTP, a remote editor, a browser and AI in one desktop workspace.
DockLab runs on macOS, Windows and Linux. Start free and keep your whole server workflow in one window.