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 losing uncommitted guest or application data even though ZFS copy-on-write keeps the on-disk pool structurally consistent. 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 supported USB or serial data interface and a host that can keep the NUT monitor running until shutdown begins. A dedicated Linux VM needs little compute, but size it for the guest OS and monitoring you actually install. 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 dedicated VM. A full VM does not have Proxmox’s privileged/unprivileged LXC distinction. 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 installs the driver, data server, and monitor components. Do not hold the packages indefinitely: NUT updates can contain hardware-support and security fixes. Stop the NUT units while you configure them if your distribution started them automatically, but do not disable them permanently.

All the files you need live under /etc/nut/:

  • nut.conf — selects which NUT layers start
  • ups.conf — driver and device definition
  • upsd.conf — data-server listener settings
  • upsd.users — credentials and monitor role
  • upsmon.conf — monitoring and shutdown behavior

For a driver, upsd, and upsmon on the same machine, /etc/nut/nut.conf is required. Its default is MODE=none, which deliberately starts nothing. Set:

MODE=standalone

The official nut.conf reference defines standalone as the local three-layer configuration.

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 No suitable USB device found, stop and troubleshoot before proceeding. Possibilities include an unsupported device, USB permissions, passthrough, the selected scanner backend, the cable, or a driver conflict; the message alone does not identify which one.

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 Tailscale rather than exposing the port directly; alternatively, 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 primary

The upsmon primary privilege allows this monitor to coordinate forced shutdown. Older NUT releases called the roles master and slave; current documentation uses primary and secondary. Protect upsd.users and upsmon.conf because they contain the password.

Start NUT through the distribution’s service manager so the components return after a reboot. On current Debian-family packages, enable and start the driver target, server, and monitor after all configuration files are in place:

systemctl enable --now nut-driver.target nut-server.service nut-monitor.service
systemctl status nut-driver.target nut-server.service nut-monitor.service

Unit names can differ on older releases; use the units installed by your package rather than leaving manually launched upsdrvctl or upsd processes as the production setup. If the driver reports permissions errors, check the package’s udev rules and that the NUT driver user can access the UPS. 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 primary
MINSUPPLIES 1
SHUTDOWNCMD "/usr/local/bin/nut-shutdown.sh"
POWERDOWNFLAG /etc/killpower
POLLFREQ 5
POLLFREQALERT 5
FINALDELAY 5

The 1 is this UPS’s power value for the monitored system, not a count of outlets. With MINSUPPLIES 1, losing the only adequate supply triggers the shutdown path once the UPS reports a critical OB LB state. The current upsmon.conf reference requires MONITOR, MINSUPPLIES, and the primary system’s power-down integration.

Next, define the shutdown command. If NUT runs directly on the Proxmox host, use the distribution’s normal shutdown command so Proxmox can stop managed guests according to their configured startup/shutdown order:

SHUTDOWNCMD "/sbin/shutdown -h now"

If NUT runs inside a VM, it must request shutdown of the hypervisor while the network and host are still available. One simple pattern is a tightly restricted SSH key whose only permitted command on the Proxmox host is systemctl poweroff, then a local script such as:

#!/bin/sh
exec ssh -o BatchMode=yes -o ConnectTimeout=10 \
  -i /root/.ssh/id_ed25519_nut root@10.0.0.1 systemctl poweroff

Do not launch several asynchronous qm shutdown commands and immediately call poweroff; that may not give guests the intended time to stop. Let Proxmox’s host shutdown orchestration honor guest ordering/timeouts, or implement an explicitly tested wait-and-timeout policy. Protect and restrict the SSH key, pin the host key, make the script root-owned and non-writable by other users, and treat a failed SSH command as an alertable shutdown failure. Do not put battery.charge or battery.runtime lines under MONITOR; that syntax is invalid. upsmon responds when the driver publishes the low-battery state (OB LB). The threshold normally comes from the UPS’s battery.charge.low or battery.runtime.low variables, which you can inspect with:

upsc ups@localhost battery.charge.low
upsc ups@localhost battery.runtime.low

If the device reports reliable charge/runtime values but its low-battery threshold is unsuitable, NUT supports cautious overrides in the UPS section of ups.conf:

[ups]
    driver = usbhid-ups
    port = auto
    desc = "Basement UPS"
    ignorelb
    override.battery.charge.low = 30
    override.battery.runtime.low = 180

ignorelb tells the driver to derive low-battery state from those thresholds instead of trusting the UPS firmware’s LB flag. The official ups.conf manual warns to use it only when the UPS reports charge/runtime reliably. Calibrate the values by measuring how long the Proxmox shutdown actually takes under load; a copied percentage is not a safety guarantee.

A forced-shutdown test is destructive: upsmon -c fsd initiates the real shutdown path and may command the UPS load off. Run it only during a maintenance window with console access, current backups, and noncritical workloads. First test USB persistence and upsc, then observe an unplugged-on-battery event without waiting for exhaustion. Finally test the complete FSD sequence and verify Proxmox gives guests enough time to stop, the host powers down, the UPS behavior matches its driver, and restart after utility power returns works as intended.

Configure the NUT VM late in Proxmox’s guest shutdown order so it remains available long enough to send the host request and record what happened. The host’s shutdown sequence will eventually stop that VM too; confirm the ordering and timeout behavior during the maintenance-window test.

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.