My homelab runs on a Proxmox host that hosts eight VMs and a handful of containers. Mail, media, DNS, a package cache, a file server -- things I actually depend on day to day. I've had a CyberPower CST1500SUC on this rack for years. What I did not have, until today, was NUT configured on it.
I rebuilt the Proxmox host from scratch last December. New install, clean slate. NUT was one of those things that worked fine on the old setup and never made it onto the new one. Not because it's complicated -- it isn't -- but because nothing had gone wrong yet. A power blip hits, every VM gets a hard reset, I think "I should really set up NUT again," and then I don't. That was five months ago.
Today I plugged in the USB cable and finished it. This post is about the software side: getting Proxmox to talk to the UPS over USB, monitoring the battery, and triggering a clean shutdown of all guests before the host goes down.
The tool for this is NUT -- Network UPS Tools. It's a mature, well-supported open source
project that handles just about every UPS you'll encounter over USB or serial. The Debian
package is nut and it installs everything you need.
apt install nut
That gives you three services: nut-driver (talks to the UPS hardware),
nut-server (the upsd daemon that exposes UPS data over a local socket),
and nut-monitor (upsmon, which watches the UPS state and triggers shutdown).
Plug the USB cable in, then scan:
nut-scanner -U
The -U flag limits the scan to USB devices. You'll get output that looks like
this (the SNMP/XML/IPMI "library not found" warnings are harmless):
Scanning USB bus.
[nutdev1]
driver = "usbhid-ups"
port = "auto"
vendorid = "0764"
productid = "0601"
product = "CST1500SUC"
serial = "CD4NP7001950"
vendor = "CPS"
bus = "007"
Note the driver, vendorid, and productid. The
driver is usbhid-ups for most modern USB UPSes -- it speaks the HID power
device protocol. The vendor and product IDs make the match reliable if you ever have
more than one USB device on the bus.
/etc/nut/ups.conf defines the UPS device. Name the device -- I used
myups -- and reference the driver and identifiers:
[myups]
driver = usbhid-ups
port = auto
vendorid = 0764
productid = 0601
desc = "CyberPower CST1500SUC"
The port = auto tells the driver to find the device by vendorid/productid
rather than by a fixed USB path, which changes after reboots and replug events.
/etc/nut/nut.conf controls which NUT components start and in what mode.
For a single machine with a directly attached UPS:
MODE=standalone
If you want other machines on your network to also monitor this UPS -- say, a NAS that
should also shut down cleanly -- change this to netserver and add a
LISTEN directive to upsd.conf. For now, standalone is correct.
/etc/nut/upsd.users defines the accounts that upsmon uses to authenticate
with upsd. You need at least one with upsmon master:
[upsmon]
password = yourpasswordhere
upsmon master
This is a local socket connection on the same machine, not exposed to the network in standalone mode, so the password mostly exists to satisfy the auth requirement.
/etc/nut/upsmon.conf tells upsmon what to monitor and what to do when things
go wrong. The MONITOR line references the device name from ups.conf
and the credentials from upsd.users:
MONITOR myups@localhost 1 upsmon yourpasswordhere master
SHUTDOWNCMD "/sbin/shutdown -h now"
POWERDOWNFLAG /etc/killpower
MINSUPPLIES 1
POLLFREQ 5
POLLFREQALERT 5
HOSTSYNC 15
DEADTIME 15
FINALDELAY 5
NOTIFYCMD /usr/local/bin/ups-notify.sh
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC
The NOTIFYCMD and NOTIFYFLAG lines are what make this
Proxmox-aware. When the UPS goes on battery (ONBATT) or the battery gets
low (LOWBATT), upsmon calls the notify script and also logs to syslog and
broadcasts a wall message. The EXEC flag is what actually invokes the script.
Without this, upsmon's default behavior on LOWBATT is to run
SHUTDOWNCMD -- which halts the host immediately, while all your VMs are still
running. They get a hard cut, same as a power outage. The whole point of this setup is to
avoid that.
The notify script needs to cleanly stop all running guests before the host can go down.
Proxmox provides command-line tools for this: qm shutdown for QEMU VMs and
pct shutdown for LXC containers. Both accept a --timeout flag.
/usr/local/bin/ups-notify.sh:
#!/bin/bash
LOGFILE=/var/log/ups-notify.log
log() { echo "$(date +"%F %T") [$NOTIFYTYPE] $*" >> "$LOGFILE"; }
if [[ "$NOTIFYTYPE" == "LOWBATT" ]]; then
log "Low battery -- shutting down VMs and CTs"
for vmid in $(qm list 2>/dev/null | awk 'NR>1 && $3=="running" {print $1}'); do
log "Shutting down VM $vmid"
qm shutdown "$vmid" --timeout 60 &
done
for ctid in $(pct list 2>/dev/null | awk 'NR>1 && $2=="running" {print $1}'); do
log "Shutting down CT $ctid"
pct shutdown "$ctid" --timeout 60 &
done
wait
log "All guests signaled; upsmon will now shut down host"
elif [[ "$NOTIFYTYPE" == "ONBATT" ]]; then
log "On battery power"
fi
chmod +x /usr/local/bin/ups-notify.sh
upsmon sets $NOTIFYTYPE to the event name before calling the script, so the
same script handles both events. On ONBATT it just logs. On LOWBATT
it fires shutdown commands for every running guest in parallel (the & at
the end of each call), then waits for all of them to finish before returning.
When the script returns, upsmon proceeds with its own shutdown sequence: running
SHUTDOWNCMD to halt the host. The host won't go down until the script exits,
so the wait at the end is what gives guests time to stop cleanly.
The --timeout 60 means Proxmox will wait up to 60 seconds for each guest to
respond to the shutdown signal before moving on. In practice, graceful OS shutdowns take
10-30 seconds; the timeout is a backstop for guests that hang.
systemctl enable nut-server nut-monitor
systemctl start nut-server nut-monitor
If you want to test the driver first before enabling the full stack:
upsdrvctl start
That starts just the driver and lets you confirm it can see the hardware.
upsc myups@localhost
You'll get a full dump of everything the driver knows about the UPS:
battery.charge: 100
battery.charge.low: 10
battery.charge.warning: 20
battery.runtime: 11425
battery.type: PbAcid
battery.voltage: 26.2
device.mfr: CPS
device.model: CST1500SUC
driver.name: usbhid-ups
driver.version.data: CyberPower HID 0.8
input.voltage: 120.0
output.voltage: 120.0
ups.load: 0
ups.mfr: CPS
ups.model: CST1500SUC
ups.realpower.nominal: 900
ups.status: OL
The two fields to check: ups.status: OL means on line (utility power is
present). battery.charge: 100 means fully charged. If the status were
OB it would be on battery, and LB would be low battery.
battery.runtime is in seconds -- 11425 seconds is about 190 minutes of runtime
at current load. My load is 0% right now because I'm not pulling anything significant through
it.
If the driver won't start, check the journal:
journalctl -u nut-server -f
The most common issue is USB permissions -- the nut user can't read the device
node. Fix it by adding nut to the plugdev group:
usermod -aG plugdev nut
Or write a udev rule to set the group on the specific device. The vendorid and productid
from nut-scanner are what you need:
# /etc/udev/rules.d/99-nut-ups.rules
SUBSYSTEM=="usb", ATTR{idVendor}=="0764", ATTR{idProduct}=="0601", GROUP="nut", MODE="0660"
udevadm control --reload-rules && udevadm trigger
On a power blip -- the kind that lasts less than 30 seconds -- the UPS absorbs it
entirely. The host sees nothing; VMs keep running; nothing in the logs except a
brief ONBATT and ONLINE event.
On a longer outage, upsmon waits until the battery hits the low threshold (10% charge by
default, configurable via battery.charge.low). That fires the notify script,
which shuts down guests one by one while there's still power to do it cleanly. When the
script returns, upsmon halts the host. Everything comes back clean when utility power
returns.
The CyberPower CST1500SUC is rated for 900W and 1500VA. My actual draw is well under that, which is why the runtime estimate is so high. At realistic homelab loads -- say 150-200W -- you get plenty of time to shut things down before the battery runs out.
I haven't done a live test yet. The right way to validate this is to yank the plug while everything is running and watch the shutdown sequence happen for real -- confirm that guests stop cleanly, that the log entries appear, that the host halts before the battery dies. I'm holding off until I've verified that all the machines I care about are actually plugged into the battery-backed outlets on the UPS rather than the surge-only outlets. Half the ports on the CST1500SUC are battery-backed; the other half are just surge protection. Plugging a server into a surge-only outlet and then wondering why NUT didn't save it is a classic mistake.
Once I've traced every cable, I'll pull the plug and write up what happened.
The tasks that got closed out today:
x 2026-05-14 install USB cable between PVE and UPS +homelab @home
x 2026-05-15 setup nut +homelab @home
Two tasks, one of which was open since May 5th. The only thing blocking it was a USB cable. It took ten days to plug in a USB cable.