A UPS without a shutdown signal is just a battery that delays the inevitable. When the power fails, your Proxmox host needs to shut down cleanly before the battery dies—otherwise you risk corrupted ZFS pools and lost VM state. NUT (Network UPS Tools) is the boring, proven way to make that happen, but the real work is getting USB passthrough right and wiring up a shutdown script that actually fires.

1. What You’ll Need (and What Will Bite You)

Start with a UPS that has a USB or serial data port and a spare USB port on the machine that will run NUT. A small Linux VM (or container) with 512 MB of RAM and a single CPU core is plenty. The traps aren’t in the shopping list; they’re in the assumptions.

Not every UPS with a USB port reports useful data. “Dumb” units may show vendor and product IDs but expose no HID‑compliant battery state—nut-scanner will see the device and then the driver will bail with a cryptic “permission denied” or “no matching HID descriptor.” Cheap USB‑to‑serial adapters built into some UPS units can vanish from the bus the moment the UPS switches to battery power. The fix is usually a different cable, but you won’t know until you test. Finally, shutting down a single host gracefully is different from shutting down a whole rack: if the UPS powers multiple machines, you need a network‑aware monitor, not just a local one.

On Proxmox, the big decision is where to run NUT: directly on the hypervisor host, or inside a privileged VM. Running on the host avoids USB passthrough entirely, but it means installing extra packages on your hypervisor and giving the shutdown script root access to the host’s qm command. Running inside a VM keeps the host clean, but you must pass the UPS’s USB device through to the guest. USB passthrough pushes you toward a full VM—LXC containers can’t do it reliably without bind‑mounting /dev/bus/usb, and that’s fragile across reboots and container restarts.

2. Installing NUT Without the Fluff

On Debian or Ubuntu, the package is as boring as it gets:

apt install nut

That pulls in nut-server, nut-client, and nut-monitor—the three pieces you’ll actually use. If you’re on a stable release, pin the version to avoid surprise updates that shuffle config syntax. For Debian Bookworm:

apt-mark hold nut nut-server nut-client nut-monitor

Immediately after install, the default services attempt to start with half‑configured config files, and they’ll fail loudly. Stop them before you touch anything:

systemctl stop nut-server nut-client nut-monitor
systemctl disable nut-server nut-client nut-monitor

You’ll re‑enable them later once the configs are correct. All the files you need live under /etc/nut/. The ones you’ll edit are:

  • ups.conf — driver and port definition
  • upsd.conf — network listener settings
  • upsd.users — credentials for monitoring
  • upsmon.conf — monitoring logic and shutdown commands

Ignore the rest for now. No need to touch nut.conf or hosts.conf unless you’re building a multi‑host setup.

2.1 The Quick Sanity Check

Before writing a single config line, confirm the UPS is visible on the USB bus. Plug it in and run:

lsusb

Look for a line like Bus 001 Device 003: ID 0463:ffff MGE UPS Systems UPS. That vendor/product pair (here 0463:ffff) is what you’ll use later for passthrough. Next, run the NUT scanner:

nut-scanner -U

Expected output shows the device and a suggested driver:

[nutdev1]
    driver = "usbhid-ups"
    port = "auto"
    vendorid = "0463"
    productid = "ffff"

If you see nothing or only “No suitable USB device found,” stop. Either the UPS is too dumb, the cable is wrong, or the kernel didn’t attach a driver. Fix that before you proceed.

3. USB Passthrough in Proxmox (No Black Magic)

There are two sane approaches: run NUT directly on the Proxmox host and skip passthrough entirely, or pass the UPS’s USB device to a dedicated VM. Running on the host is simpler but means the shutdown script lives on the hypervisor itself. If you prefer isolation, pass the device to a VM.

Identify the USB device on the Proxmox host with lsusb and note the bus and device numbers, e.g., Bus 001 Device 003. Then grab the vendor and product IDs:

lsusb -s 001:003
Bus 001 Device 003: ID 0463:ffff MGE UPS Systems UPS

Now add the device to the VM’s configuration. For VM ID 100:

qm set 100 -usb0 host=0463:ffff

That’s it. The host= syntax matches by vendor/product ID, which survives USB renumbering across reboots—the most common persistence gotcha. Using bus/device numbers instead will break the next time you reboot or unplug the cable.

Inside the guest, verify the device appears:

lsusb

You should see the same vendor:product line. If the guest is a Linux VM, the usbhid driver will grab it automatically; no extra drivers needed. LXC containers can’t use this method reliably because they lack a kernel‑independent /dev/bus/usb. Bind‑mounting that path from the host is possible but fragile, so stick with a VM.

4. Wiring Up NUT: Driver, upsd, and the First Monitor

Start with /etc/nut/ups.conf. Define a UPS named ups using the driver and port from the scanner:

[ups]
    driver = usbhid-ups
    port = auto
    desc = "Basement UPS"

Some units need vendorid and productid explicitly if auto doesn’t work, but start simple. Next, /etc/nut/upsd.conf—keep it locked down. Listen only on localhost unless you have a network‑wide monitoring setup:

LISTEN 127.0.0.1 3493
LISTEN ::1 3493

No 0.0.0.0 here. If you need to check UPS status remotely, tunnel through a VPN like WireGuard rather than exposing the port directly; see WireGuard VPN: Routing, Kill Switches & DNS Leaks. Then create a dedicated user in /etc/nut/upsd.users that will run the monitor:

[monuser]
    password = a_long_random_string
    upsmon master

The upsmon master privilege means this user can initiate a forced shutdown. Use a long random string; it’s not exposed to the network if you’re listening on localhost, but don’t be lazy.

Now start the driver and the data server:

upsdrvctl start
upsd

If upsdrvctl complains about permissions, check that the nut user can access the USB device (the nut-client package usually sets up udev rules). Once upsd is running, query the live data:

upsc ups@localhost

You’ll see a wall of variables. The critical ones for shutdown logic are:

  • battery.charge — percentage remaining
  • battery.runtime — estimated seconds left
  • ups.status — something like OL (on line) or OB DISCHRG (on battery, discharging)

If battery.charge shows 0 or battery.runtime is missing, your UPS doesn’t report that data. You’ll have to fall back to a fixed time‑based shutdown or a simple ups.status check, which is riskier.

5. The Shutdown Script That Actually Runs

upsmon is the piece that watches the UPS and fires commands when things go wrong. Edit /etc/nut/upsmon.conf and set up the MONITOR line:

MONITOR ups@localhost 1 monuser a_long_random_string master

The 1 is the number of power supplies this UPS feeds (usually 1). The master role means this system will initiate the shutdown; slaves would wait for a signal from the master.

Next, define the shutdown command. This is where the script has to reach the Proxmox host before the VM loses power. The simplest reliable method: SSH from the NUT VM into the Proxmox host with an SSH key, then run qm shutdown for every critical VM, followed by a host poweroff. Create a script, say /usr/local/bin/nut-shutdown.sh:

#!/bin/bash
# SSH to Proxmox host and shut down VMs, then the host
ssh -i /root/.ssh/id_rsa_nut [email protected] "for vmid in 100 101 102; do qm shutdown \$vmid; done; poweroff"

Make it executable and owned by root, mode 700. Then set SHUTDOWNCMD in upsmon.conf:

SHUTDOWNCMD "/usr/local/bin/nut-shutdown.sh"

The timing comes from two thresholds in the same file. The most important numbers are DEADTIME (seconds without contact before declaring the UPS dead) and the thresholds you’ll set in the MONITOR line’s options. Add a battery.charge floor and a battery.runtime minimum directly to the MONITOR line:

MONITOR ups@localhost 1 monuser a_long_random_string master
        battery.charge 30
        battery.runtime 120

That tells upsmon to start the shutdown sequence when the battery falls below 30% or when the remaining runtime drops under 120 seconds, whichever comes first. upsmon will then run SHUTDOWNCMD after a short internal delay (FINALDELAY seconds, default 5). The script must complete before the UPS cuts power, so keep it tight.

Test with upsmon -c fsd (forced shutdown) while the UPS is on mains power. That triggers the full shutdown sequence without actually draining the battery. Watch the Proxmox host’s logs to confirm the VMs shut down and the host powers off. After that, plug the UPS back into mains and everything should come back up when power returns—assuming you’ve set Proxmox’s BIOS to power on after AC loss.

A final warning: if the NUT VM itself is one of the VMs being shut down by qm shutdown, you need to order the shutdowns so the NUT VM dies last. Otherwise the script kills its own host mid‑execution. Either exclude the NUT VM from the shutdown list or shut it down last and accept that the final poweroff command on the Proxmox host will cut its remaining power.

A working NUT setup is unremarkable. It sits there for months, polls the UPS, and then one night the power fails and everything shuts down cleanly. The hard part isn’t the software—it’s verifying that the USB passthrough survives a reboot, that the driver reads real battery data, and that the shutdown script actually fires when the battery hits the threshold you set. Test with the UPS unplugged, watch the logs, and don’t trust a config you haven’t seen run end‑to‑end. Boring and proven beats exciting and broken every time.