proxmox-upgrade-cluster.sh is a Bash script that performs a rolling upgrade of
a Proxmox cluster. It automates the upgrade and optional reboot of one or more
nodes.
- Automatically determine cluster members (
-c HOSTNAME). - Upgrade all or specific nodes (
-n HOSTNAME). - When discovering nodes via
-c, upgrade them in ascending order of running guest count so the lightest node migrates first (override with--preserve-discovery-order). - Migrate running guests off each node via Proxmox HA maintenance mode before
upgrading (override with
--no-maintenance-mode). - Pre-flight and during-upgrade safety checks (cluster reachable, no offline members, no running tasks or guests on the target).
- Reboot nodes only when needed, or can optionally force a reboot (
--force-reboot). - Optionally force package reinstallation after upgrade (
--pkg-reinstall PACKAGE). - No-op mode (
--dry-run). - Reuse a single SSH connection per host via connection multiplexing
(
ControlMaster/ControlPath/ControlPersist), so the many per-node commands avoid a fresh TCP and auth handshake each time (disable with--no-ssh-multiplexing). - Multiple levels of verbosity (
-v,-vv,-vvv).
- Bash 4.4 or newer
- SSH access to all Proxmox nodes
- jq installed on your system and on
PATH.
- Cluster HA is enabled with all guests managed.
- SSH is available to all nodes (outside of reboots) to all nodes while the running.
- That running this script is not stopped mid-run. Currently, it doesn't to any cleanup or checking to make sure the cluster is left in a working state. Manual intervention may be needed if exiting or errors occur.
- SSH Key Auth is set up and working for the root user. Users other than root have not been tested.
- No rebalancing or guest migration outside the cluster HA. Recommended rebalancing after either manually or with other tools.
NAME
proxmox-upgrade-cluster.sh - Perform a rolling upgrade for a Proxmox cluster
SYNOPSIS
proxmox-upgrade-cluster.sh [OPTIONS] --cluster-node HOSTNAME
proxmox-upgrade-cluster.sh [OPTIONS] --node HOSTNAME [--node HOSTNAME]...
proxmox-upgrade-cluster.sh --help
OPTIONS
-c HOSTNAME, --cluster-node HOSTNAME
A node in a cluster to pull all nodes from.
-n HOSTNAME, --node HOSTNAME
Node(s) to upgrade. Can be passed multiple times.
-u USER, --ssh-user USER
SSH user to authenticate with. Defaults to "root".
-o SSH_OPT, --ssh-opt SSH_OPT
Options to pass to ssh. Can be passed multiple times.
--ssh-allow-password-auth
Allow ssh password auth. Default is to force SSH key auth with
'PasswordAuthentication=no'.
--no-ssh-multiplexing
Disable SSH connection multiplexing. By default a single master
connection per host is reused (ControlMaster/ControlPath/ControlPersist)
so the many per-node commands avoid a fresh TCP+auth handshake each time.
--cluster-node-use-ip
When using '--cluster-node', use the IP address instead of the node name.
--dry-run
Enable dry-run mode; no destructive actions are taken.
--pkg-reinstall PACKAGE
Package(s) on the hosts to reinstall with 'apt-get reinstall' post
upgrade. Can be passed multiple times. Defaults to none.
--ignore-task-type TYPE
Task type(s) to ignore when checking for running cluster tasks.
Can be passed multiple times. Default of 'vncproxy termproxy spiceproxy'.
--force-upgrade
Force all nodes to upgrade, and not only those with available upgrades.
--reboot-only
Skip apt-update and apt-get dist-upgrade entirely. Filter
cluster_nodes via node_needs_reboot and reboot just the ones
running a kernel older than the one staged for boot. Mutually
exclusive with --force-upgrade, --force-reboot, and
--skip-reboot.
--no-maintenance-mode
Don't set node to maintenance mode when upgrading. This will disable
HA migrations. Default: maintenance mode is enabled.
--force-reboot
Force all nodes to be rebooted during upgrade, and not only those that
aren't booted with the same kernel as the currently installed one.
Mutually exclusive with --skip-reboot.
--skip-reboot
Skip the reboot step entirely, even when a new kernel is staged for
boot. Mutually exclusive with --force-reboot.
--reboot-timeout SECONDS
Maximum number of seconds to wait for a node to reboot and come back
up before aborting the upgrade. Defaults to 900 (15 minutes).
--allow-running-guests
Disable check for running guests on the node prior to upgrade.
--allow-running-tasks
Disable check for running tasks on the cluster prior to upgrade.
--preserve-discovery-order
When using --cluster-node, do not reorder the upgrade sequence
ascending by running guest count. Default is to sort so the node
with the fewest guests upgrades first.
-v, --verbose
Log actions and details. Each repetition unlocks an additional log
level (all lower levels stay enabled):
(none) INFO — status, warnings, errors
-v + VERBOSE — high-level script-flow notes
-vv + DEBUG — per-node ssh commands as they run
-vvv + DEBUG2 — full pvesh JSON responses; timestamps
also gain millisecond precision
-vvvv + DEBUG3 — internal background-job lifecycle
Levels beyond -vvvv are for script development:
-vvvvv also pass -v to ssh (remote login verbose)
-vvvvvv also enable bash 'set -x' (command trace)
-vvvvvvv also pass another -v to ssh (ssh -vv)
-h, --help
Show this message.
EXAMPLE
Upgrade all nodes in a cluster, retrieving the cluster nodes from 'pve1':
proxmox-upgrade-cluster.sh -c pve1
Upgrade only nodes pve2, pve3:
proxmox-upgrade-cluster.sh -n pve2 -n pve3
Reach the cluster through a bastion via an ssh ProxyCommand. Pass each
ssh option as its own --ssh-opt value:
proxmox-upgrade-cluster.sh -c pve1 \
--ssh-opt "-o ProxyCommand=ssh -W %h:%p bastion.example.com" ./proxmox-upgrade-cluster.sh -c pve1 -vv --dry-run
[2026-05-27 17:02:36][INFO ] Running in dry run mode.
[2026-05-27 17:02:36][INFO ] Getting all cluster nodes from node 'pve1'...
[2026-05-27 17:02:36][DEBUG ][pve1] Running command 'whoami'
[2026-05-27 17:02:36][DEBUG ][pve1] Node is up.
[2026-05-27 17:02:36][DEBUG ][pve1] Running command 'hash pvesh'
[2026-05-27 17:02:36][DEBUG ][pve1] Running command 'pvesh get cluster/status --output-form=json'
[2026-05-27 17:02:38][INFO ] Using 'pve1 pve3 pve2' as all nodes to check.
[2026-05-27 17:02:38][INFO ] Checking if any nodes are currently down...
[2026-05-27 17:02:38][DEBUG ][pve1] Running command 'whoami'
[2026-05-27 17:02:38][DEBUG ][pve3] Running command 'whoami'
[2026-05-27 17:02:38][DEBUG ][pve2] Running command 'whoami'
[2026-05-27 17:02:38][DEBUG ][pve1] Node is up.
[2026-05-27 17:02:38][DEBUG ][pve2] Node is up.
[2026-05-27 17:02:38][DEBUG ][pve3] Node is up.
[2026-05-27 17:02:38][INFO ] All nodes are up.
[2026-05-27 17:02:38][INFO ] Checking if any nodes are not proxmox...
[2026-05-27 17:02:38][DEBUG ][pve1] Running command 'hash pvesh'
[2026-05-27 17:02:38][DEBUG ][pve3] Running command 'hash pvesh'
[2026-05-27 17:02:38][DEBUG ][pve2] Running command 'hash pvesh'
[2026-05-27 17:02:38][INFO ] All nodes are proxmox.
[2026-05-27 17:02:38][INFO ] Checking if any nodes are currently not online...
[2026-05-27 17:02:38][DEBUG ][pve1] Running command 'pvesh get cluster/ha/status/manager_status --output-form=json'
[2026-05-27 17:02:40][INFO ] All nodes are online.
[2026-05-27 17:02:40][INFO ] Checking if any nodes currently have tasks running...
[2026-05-27 17:02:40][DEBUG ][pve1] Running command 'pvesh get nodes/$(hostname)/tasks --source=active --output-form=json'
[2026-05-27 17:02:40][DEBUG ][pve3] Running command 'pvesh get nodes/$(hostname)/tasks --source=active --output-form=json'
[2026-05-27 17:02:40][DEBUG ][pve2] Running command 'pvesh get nodes/$(hostname)/tasks --source=active --output-form=json'
[2026-05-27 17:02:41][DEBUG ][node_not_running_task][pve2] Task Count: 0
[2026-05-27 17:02:41][DEBUG ][node_not_running_task][pve3] Task Count: 0
[2026-05-27 17:02:41][DEBUG ][node_not_running_task][pve1] Task Count: 0
[2026-05-27 17:02:41][INFO ] No tasks are running.
[2026-05-27 17:02:41][INFO ] Checking for updates on all nodes...
[2026-05-27 17:02:41][DEBUG ][pve1] Running command 'DEBIAN_FRONTEND=noninteractive apt-get update'
[2026-05-27 17:02:41][DEBUG ][pve3] Running command 'DEBIAN_FRONTEND=noninteractive apt-get update'
[2026-05-27 17:02:41][DEBUG ][pve2] Running command 'DEBIAN_FRONTEND=noninteractive apt-get update'
[2026-05-27 17:02:41][VERBOSE][pve2][apt] Hit:1 https://deb.debian.org/debian trixie InRelease
[2026-05-27 17:02:41][VERBOSE][pve3][apt] Hit:1 https://deb.debian.org/debian trixie InRelease
[2026-05-27 17:02:41][VERBOSE][pve1][apt] Hit:1 https://deb.debian.org/debian trixie InRelease
[2026-05-27 17:02:41][VERBOSE][pve2][apt] Hit:2 https://deb.debian.org/debian trixie-updates InRelease
[2026-05-27 17:02:41][VERBOSE][pve3][apt] Hit:2 https://deb.debian.org/debian trixie-updates InRelease
[2026-05-27 17:02:41][VERBOSE][pve1][apt] Hit:2 https://deb.debian.org/debian trixie-updates InRelease
[2026-05-27 17:02:41][VERBOSE][pve3][apt] Hit:3 https://deb.debian.org/debian-security trixie-security InRelease
[2026-05-27 17:02:41][VERBOSE][pve1][apt] Hit:3 https://deb.debian.org/debian-security trixie-security InRelease
[2026-05-27 17:02:41][VERBOSE][pve2][apt] Hit:3 https://deb.debian.org/debian-security trixie-security InRelease
[2026-05-27 17:02:41][VERBOSE][pve3][apt] Hit:4 https://boomshankerx.github.io/proxmox-truenas-apt trixie InRelease
[2026-05-27 17:02:41][VERBOSE][pve1][apt] Hit:4 https://boomshankerx.github.io/proxmox-truenas-apt trixie InRelease
[2026-05-27 17:02:41][VERBOSE][pve2][apt] Hit:4 https://boomshankerx.github.io/proxmox-truenas-apt trixie InRelease
[2026-05-27 17:02:43][VERBOSE][pve2][apt] Hit:5 http://download.proxmox.com/debian/ceph-squid trixie InRelease
[2026-05-27 17:02:43][VERBOSE][pve3][apt] Hit:5 http://download.proxmox.com/debian/ceph-squid trixie InRelease
[2026-05-27 17:02:43][VERBOSE][pve1][apt] Hit:5 http://download.proxmox.com/debian/ceph-squid trixie InRelease
[2026-05-27 17:02:43][VERBOSE][pve2][apt] Hit:6 http://download.proxmox.com/debian/pve trixie InRelease
[2026-05-27 17:02:43][VERBOSE][pve3][apt] Hit:6 http://download.proxmox.com/debian/pve trixie InRelease
[2026-05-27 17:02:43][VERBOSE][pve1][apt] Hit:6 http://download.proxmox.com/debian/pve trixie InRelease
[2026-05-27 17:02:43][VERBOSE][pve3][apt] Reading package lists...
[2026-05-27 17:02:43][VERBOSE][pve2][apt] Reading package lists...
[2026-05-27 17:02:43][VERBOSE][pve1][apt] Reading package lists...
[2026-05-27 17:02:43][DEBUG ][pve1] Running command 'DEBIAN_FRONTEND=noninteractive apt-get -qq -s upgrade'
[2026-05-27 17:02:44][INFO ][pve1] No updates available.
[2026-05-27 17:02:44][VERBOSE][pve1] Removed from upgrade sequence.
[2026-05-27 17:02:44][DEBUG ][pve3] Running command 'DEBIAN_FRONTEND=noninteractive apt-get -qq -s upgrade'
[2026-05-27 17:02:44][INFO ][pve3] No updates available.
[2026-05-27 17:02:44][VERBOSE][pve3] Removed from upgrade sequence.
[2026-05-27 17:02:45][DEBUG ][pve2] Running command 'DEBIAN_FRONTEND=noninteractive apt-get -qq -s upgrade'
[2026-05-27 17:02:45][INFO ][pve2] No updates available.
[2026-05-27 17:02:45][VERBOSE][pve2] Removed from upgrade sequence.
[2026-05-27 17:02:45][INFO ] No nodes need updates. Exiting.
Tests are written using ShellSpec and run via Nix development shell:
nix develop -c shellspec spec/Run a specific test file or tag:
nix develop -c shellspec spec/logging_spec.sh
nix develop -c shellspec spec/node_functions_spec.sh:@1-1Tests mock all SSH and external commands. No Proxmox cluster is required to run the suite.
This script is provided as-is under the GNU v3 License. Use at your own risk.