UPS Monitoring on Proxmox with NUT

May 14, 2026

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.

Install

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).

Detect the UPS

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.

ups.conf

/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.

nut.conf

/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.

upsd.users

/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.

upsmon.conf

/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 Proxmox Shutdown Hook

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.

Start and Enable

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.

Verify

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.

Troubleshooting

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

What This Buys

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.

What's Next

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.


back