One command, many servers.
The rest of DiskPush moves bytes to a server. Fleet moves work to a set of them — a package upgrade, a health check, a script you already have — and reports every server separately.
diskpush fleet check --on tag:production
diskpush fleet upgrade --on tag:production --sudo
diskpush fleet run "systemctl reload nginx" --on 'web-*' --sudo
diskpush fleet script ./rotate-keys.sh --on all '!db-01'In the desktop app it is the Fleet tab, beside Transfer: tick the servers on the left, type a command or pick a recipe, watch each host report on its own. See desktop.md.
--on takes any number of terms, repeated or comma-separated. They are the
same terms the desktop's checkboxes and tag filter produce.
all every server DiskPush knows about
web-01 one, by name (or by id)
web-* a glob over names, `*` and `?` only
tag:production every server carrying that tag
host:10.0.0.* a glob over hostnames
!web-03 remove one from the set
!tag:canary remove a whole tag from the set
Includes are unioned, then exclusions are subtracted, so order does not
matter: tag:web !web-03 and !web-03 tag:web select the same servers.
A tag says what a server is. A list is a set you assembled by hand and want back — "the four boxes behind the EU load balancer" is not a property of any one of them.
diskpush fleet lists save eu-edge --on 'web-*,cache-01' --description "behind the EU LB"
diskpush fleet lists # read
diskpush fleet lists show eu-edge
diskpush fleet lists rename eu-edge edge
diskpush fleet lists remove edge # the servers themselves are untouched
diskpush fleet run "uptime" --on list:eu-edge
diskpush fleet upgrade --on 'list:eu-edge,!web-03' --sudolist: is a prefix so a list and a server may share a name without either
shadowing the other: --on production is the server, --on list:production
is the list.
A list stores each member's connection id and the name it had when saved. The id is what resolves, so a renamed server stays in the list. The name is what keeps the list readable afterwards — and a member whose connection has been deleted is named and refused, not silently skipped:
The list "pair" names 1 server(s) that no longer exist: web-02.
Save the list again to drop them.
That is the same rule a selector term follows. A list that quietly got smaller is how a command misses the one server it most needed to reach.
Saving resolves the selector now and stores the result. A list is a set someone chose, not a query that might mean something different next week — use a tag when you want the dynamic behaviour.
In the desktop app the lists appear as chips at the top of the server sidebar, above the tags. Clicking one ticks exactly its members; ticking servers by hand offers Save these 3; the × on a chip deletes the list and leaves the servers alone.
Both saved connections and ~/.ssh/config hosts are selectable. A saved
connection wins a name clash. diskpush fleet servers lists what is available
and where each entry came from.
A term that matches nothing is an error, not a smaller fleet. --on web-O3
with a letter O fails rather than quietly upgrading eleven of your twelve
servers. The same is true of an exclusion: !web-O3 was meant to protect a
server, and silently protecting none is worse than stopping.
Asks every selected server what it needs. Installs nothing, takes no package manager lock, and needs no root, so it is safe to run at any hour.
diskpush fleet check --on allSERVER OS PM UPD SEC REBOOT DISK UPTIME
------- ------------------ --- --- --- ------ ---- ------
web-01 Ubuntu 24.04.1 LTS apt 14 3 YES 61% 142d
web-02 Ubuntu 24.04.1 LTS apt 0 0 no 48% 3d
db-01 Rocky Linux 9.4 dnf 7 1 no 72% 9d
A count DiskPush could not obtain shows as ?, and one this package manager
cannot report at all shows as -. Neither is ever rendered as 0: "nothing
pending" is a claim, and it has to be earned.
--pending hides the servers with nothing to do.
Installs what check found.
diskpush fleet upgrade --on tag:production --sudo --concurrency 2The package manager is detected on each host, inside the script, so one
command covers a fleet that mixes Debian, Rocky and Alpine. apt, dnf,
yum, zypper, pacman, apk, brew and pkg are driven.
Every invocation is non-interactive and answers "keep the installed config
file" where the question comes up, because a fleet upgrade that stops on a
conffile prompt on host four has already failed. Nothing is removed:
upgrade, never dist-upgrade or autoremove, because both can take away
something that is running.
Rebooting is off by default; the run tells you which servers need one.
diskpush fleet upgrade --on tag:web --sudo --reboot # only those that need it
diskpush fleet upgrade --on tag:web --sudo --reboot=always # all of themEither form names the servers it will restart and asks first, unless --yes.
diskpush fleet run "df -h /" --on all
diskpush fleet run --command reload-nginx --on tag:web
diskpush fleet script ./deploy.sh --on tag:web --sudorun with a quoted string runs it as a single command line. script reads a
local file and pipes it to sh — or to bash, if the file starts with a
#! line naming it.
A script runs under sh -e: it stops at the first failing command, which is
what someone writing a three-line deploy step assumes already happens.
For a probe, whose commands fail as part of doing their job — grep -c
exits 1 when it counts nothing — either pass --no-fail-fast, or open the
script with set +e. Prefer set +e: it travels with the script, so the
same text is still correct after it is copied, saved as a command, or picked
from the desktop's recipe list, none of which carry a flag. The shipped
check-updates recipe does exactly that.
Saved commands, plus the recipes DiskPush ships (check-updates, upgrade,
reboot-required, disk, uptime, who).
diskpush fleet commands
diskpush fleet commands save reload-nginx "systemctl reload nginx" --sudo --on tag:web
diskpush fleet commands copy upgrade my-upgrade # built-ins are copied, not edited
diskpush fleet run --command reload-nginx # --on comes from the saved defaultA saved command remembers the settings, not just the script: the interpreter, whether it runs through sudo, the timeout, how many servers at a time, and whether a failure stops the rest. Picking one restores all of it. Those last two matter — "reload nginx" and "upgrade the database tier" want very different answers, and re-choosing them on every run is how a saved command still gets run wrong.
diskpush fleet commands save careful "systemctl reload nginx" \
--sudo --concurrency 2 --stop-on-error --timeout 45 --on tag:web
diskpush fleet run --command careful # 2 at a time, stops on failure
diskpush fleet run --command careful --concurrency 8 # a flag still winsIn the desktop app the same commands are the strip above the editor. Editing anything offers Save these settings; a saved command carries an × to delete it. A shipped recipe has no × — it is copied, not edited, so upgrading DiskPush never silently changes a command you rely on.
A saved command with the same name as a built-in shadows it.
Every run is recorded before it starts, and each host's result is written as it finishes — so a run you interrupt still has the results it collected.
diskpush fleet runs
diskpush fleet show af4769db # full output for the hosts that failed
diskpush fleet show af4769db --all # and for the ones that did notA run stores the script it ran, not a pointer to the command it came from. Editing a saved command does not rewrite the history of what was executed on production last Tuesday. It stores the server's name and hostname the same way, so a renamed or deleted connection does not turn a post-mortem into "the host that used to be id 7f3a".
Concurrency is bounded, four at a time by default (--concurrency).
Forty simultaneous SSH connections is a way to get rate-limited by your own
bastion.
Every host has a deadline, 900 seconds by default and 3600 for an upgrade
(--timeout). A host still running at the deadline is signalled and reported
as timeout, not as a failure of the command.
Failure does not stop the fleet unless you ask. --stop-on-error stops
queuing further servers; hosts already running are left to finish, because
killing a package manager mid-transaction to enforce a policy about a
different server leaves a broken dpkg behind.
Ctrl-C stops a run rather than killing the process: servers already running are signalled, the rest are cancelled, and everything collected so far is still recorded.
| State | What it means |
|---|---|
succeeded |
The command ran and exited 0. |
failed |
The command ran and exited non-zero. Its own exit code is recorded. |
unreachable |
DiskPush never got a session. The command did not run. |
timeout |
Still running at the deadline. |
cancelled |
The run was stopped before this host finished, or before it started. |
Collapsing unreachable into failed is how a fleet tool ends up reporting a
server that was simply switched off as a failed deploy.
--sudo runs through sudo -n, which fails immediately when a password is
wanted rather than hanging forever on a prompt nobody can see. That failure is
reported as what to do about it, not as sudo's own wording.
diskpush fleet run "systemctl restart nginx" --on tag:web --sudo
diskpush fleet run "systemctl restart nginx" --on tag:web --sudo-password--sudo-password asks once, without echo, and feeds it to sudo -S on each
host's stdin. It is held in memory for the length of the run: it is never
written to the database, never logged, and never part of the command line —
so it cannot appear in ps output on the server either.
Script text goes to the remote interpreter on stdin. The command line only
ever names the interpreter (/bin/sh -es). A quote, a backtick or a newline
in your script cannot become a different command than the one you wrote —
your bytes arrive as your bytes.
--interpreter raw is the exception, for uptime and
systemctl status nginx, where wrapping the text is more ceremony than the
job needs.
A script matching a known way to lose a machine does not fan out until you confirm it — the same bargain as Mirror on the transfer side, which always shows the delete list first.
$ diskpush fleet run "rm -rf $CACHE/" --on tag:web
This command matches a pattern that can destroy a server:
line 1: Recursive forced delete. On the wrong path this removes the system.
rm -rf $CACHE/
It would run on 6 server(s).
Run it anyway? [y/N]
Caught: recursive deletes of / (including the "$UNSET/" form, which is how
this usually happens), mkfs and friends, raw writes to block devices, power
commands, changes to SSH or the firewall, permission resets from the root
down, account deletion, fork bombs, piping a URL into a shell, and DROP DATABASE.
This is a tripwire, not a sandbox. A shell is Turing-complete and anything here can be written around in ten seconds. It catches the accident, and the accident is what actually happens.
In the desktop app the same check runs in the main process, not the renderer: the dialog showing you a confirmation is what makes it true, and a renderer that skipped the dialog cannot skip the check with it.
A fan-out is the wrong moment to be answering host key prompts one at a time,
so a host that is not in known_hosts fails rather than asking. Check it once
with diskpush connections test NAME, or pass --accept-new deliberately for
that run. A changed key is never accepted, by --accept-new or anything
else.
0 when every server succeeded, 71 when any did not. Deliberately one code
rather than the failing host's own status: across twelve servers there may be
several, and picking one to pass through would mean inventing a winner.
--json carries every host's real exit code, stdout, stderr and duration.
diskpush fleet run "nginx -t" --on tag:web --json | jq '.hosts[] | select(.state != "succeeded")'