Your UPS Is Lying to You (By Omission)
It’s 2 AM. A storm rolls through. Your UPS beeps twice, holds for eight minutes, then cuts out when the battery finally gives up. Your servers hard-power off. You wake up the next morning, coffee in hand, and discover your ZFS pool is in a degraded state because one of the drives was mid-write when the lights went out.
The UPS did exactly what it was supposed to: buy time. The problem? Nothing was watching it. There was no daemon, no notification, no countdown — just silence until the battery ran dry and physics took over.
That’s the gap NUT fills.
Network UPS Tools (NUT) is a decades-old, battle-tested open source project that talks to your UPS over USB or serial, exposes every stat (battery charge, runtime remaining, load percentage, input voltage, temperature) over the network, and — crucially — tells your hosts to shut down gracefully before the battery dies. One NUT server. Multiple clients. Zero corrupt filesystems.
The Architecture: Three Daemons, One Job
NUT splits the work cleanly:
upsd— the server. Talks to the UPS hardware via a driver, then listens on a TCP port (default: 3493) so other hosts can query it.upsmon— the client. Runs on every machine you want to protect. Connects toupsd, watches for theOB LB(on battery, low battery) state, and triggers a clean shutdown.- Drivers — the hardware translators.
usbhid-upscovers most modern USB UPSes (APC, CyberPower, Eaton). Older APC serial units useapcsmart. There’s a driver for basically everything.
The clean architecture means your NUT server can be your Proxmox host, a Pi, a cheap VM — whatever has a USB cable plugged into the UPS. Everything else just calls home over TCP.
Step 1: Find Your Driver
Don’t guess. Run discovery:
sudo apt install nut nut-clientsudo nut-scanner -Unut-scanner probes USB for UPS devices and spits out a ready-to-paste config block. If it finds your APC Back-UPS, you’ll see something like:
[apc-backups] driver = usbhid-ups port = auto desc = "APC Back-UPS 1500"usbhid-ups handles the overwhelming majority of modern USB UPSes. The old apcupsd-era apcsmart driver is for serial-connected APC units with the smart cable — you probably don’t have one. If nut-scanner returns nothing, check lsusb to confirm the UPS is even seen by the OS, then look up your model in the NUT hardware compatibility list.
One gotcha: cheap “smart” UPSes that are actually dumb offline UPSes. They’ll enumerate as USB HID devices, the driver will load, but you’ll get nonsense readings — 99% battery charge forever, no runtime estimate. NUT can’t fix the hardware. A manufacturer: Generic in the output is your early warning sign.
Step 2: Configure the Server
Three config files. All live in /etc/nut/.
ups.conf — Define the UPS
[myups] driver = usbhid-ups port = auto desc = "CyberPower CP1500PFCLCD" pollinterval = 2The name in brackets (myups) becomes the identifier you reference everywhere else. Keep it short.
upsd.conf — Network Binding
By default, upsd only listens on localhost. If you want client machines to connect, open it up:
LISTEN 0.0.0.0 3493MAXAGE 15MAXAGE is how stale a data reading can get (in seconds) before upsd considers the connection to the driver dead. Default is 15. Don’t crank it high — if the driver crashes, you want to know fast.
upsd.users — Auth for Clients
[upsmon] password = changeme_please upsmon master
[readonly] password = readonlypw actions = SET instcmds = ALL upsmon slavemaster means this host initiates the final shutdown. slave means it shuts down first, then waits for master to finish. On a single-host setup, you’re always master.
nut.conf — Mode
MODE=standaloneOptions: standalone (server + client on same box), netserver (server only, clients elsewhere), netclient (client only). If you’re running a dedicated NUT server box, use netserver. If everything’s on one machine, use standalone.
Start It Up
sudo systemctl enable nut-server nut-monitorsudo systemctl start nut-server nut-monitorsudo upsc myups@localhostThat last command should dump a wall of metrics. If you see battery.charge: 100 and ups.status: OL (online), you’re talking to the UPS.
Step 3: Configure the Clients (upsmon.conf)
On the NUT server itself, and on every other host you want to protect:
MONITOR [email protected]:3493 1 upsmon changeme_please master
MINSUPPLIES 1SHUTDOWNCMD "/sbin/shutdown -h now"NOTIFYCMD /usr/sbin/upsschedPOLLFREQ 5POLLFREQALERT 5HOSTSYNC 15DEADTIME 15POWERDOWNFLAG /etc/killpower
NOTIFYMSG ONLINE "UPS %s: back on line power"NOTIFYMSG ONBATT "UPS %s: on battery"NOTIFYMSG LOWBATT "UPS %s: battery is low"NOTIFYMSG COMMBAD "UPS %s: lost contact"
NOTIFYFLAG ONLINE SYSLOG+WALLNOTIFYFLAG ONBATT SYSLOG+WALL+EXECNOTIFYFLAG LOWBATT SYSLOG+WALL+EXECNOTIFYFLAG COMMBAD SYSLOG+WALLThe MONITOR line format is upsname@host:port numpower username password role.
On slave machines (nodes that should shut down before the server), change master to slave. Slaves shut down first so they don’t get stranded waiting for a share from a host that’s already gone dark.
SHUTDOWNCMD is exactly what it sounds like. On Proxmox, you might want something fancier that gracefully migrates VMs first — more on that in a moment.
Docker Compose: NUT Server in a Container
If you want to run the NUT server as a container (useful on systems where you’d rather not install system daemons), here’s a working setup:
Full example: Clone the working files at github.com/KingPin/sumguy-examples/self-hosting/nut-ups-power-monitoring/
services: nut-upsd: image: instantlinux/nut-upsd:latest container_name: nut-upsd restart: unless-stopped ports: - "3493:3493" devices: - /dev/bus/usb:/dev/bus/usb environment: - API_USER=upsmon - API_PASSWORD=changeme_please - DESCRIPTION=My Home Lab UPS - DRIVER=usbhid-ups - NAME=myups - PORT=auto - SERIAL= volumes: - nut-data:/etc/nut
volumes: nut-data:The devices passthrough is the critical piece — without it, the container can’t see the USB UPS. Run lsusb on the host first to confirm the device is there. If you’re on Proxmox and passing USB through to an LXC, you’ll need to add the USB device to the LXC config instead.
The container handles server duties. Install nut-client natively on each host that needs to respond to shutdown signals — upsmon needs to run as a proper system daemon to actually trigger a shutdown.
Integrations: Where It Gets Fun
Home Assistant
HA has a built-in NUT integration (Settings → Devices & Services → Add Integration → NUT). Point it at your NUT server’s IP and port. You’ll get battery charge, runtime, load, input/output voltage all as sensors. Wire them into an automation if you want a Slack message or push notification when the UPS kicks in — because your phone staying silent during a power outage is how you sleep through a 6-hour battery drain.
Prometheus + Grafana
The nut-exporter Prometheus exporter (by DRuggeri) scrapes upsc output and exposes it on :9995/metrics. Add it to your Compose stack:
nut-exporter: image: druggeri/nut_exporter:latest container_name: nut-exporter restart: unless-stopped ports: - "9995:9995" environment: - NUT_EXPORTER_SERVER=nut-upsd - NUT_EXPORTER_USERNAME=readonly - NUT_EXPORTER_PASSWORD=readonlypwThen scrape it in your Prometheus config and build a dashboard. Watching battery runtime trend down over years tells you when it’s time to replace the cells — usually around the 3-year mark for VRLA batteries, though the cheap ones start lying to you earlier.
Proxmox UPS Integration
Proxmox has native UPS support in the Datacenter → UPS section, or you can run upsmon directly on the PVE host. The smarter approach is a custom SHUTDOWNCMD that gracefully shuts down VMs before the host goes down:
#!/bin/bashfor vmid in $(qm list | awk 'NR>1 {print $1}'); do qm shutdown $vmid --timeout 60donesleep 30/sbin/shutdown -h nowPoint SHUTDOWNCMD at that script. Your VMs get 60 seconds to flush writes, then the host shuts clean. Adjust the timeout based on how long your battery realistically lasts past the LOWBATT threshold.
NUT vs. apcupsd
If you’ve been around long enough, you know apcupsd. It’s been handling APC UPS monitoring since the mid-90s and it works fine — if you have APC hardware and only one host.
Here’s the honest comparison:
| NUT | apcupsd | |
|---|---|---|
| Vendor support | APC, CyberPower, Eaton, Tripplite, ~150 more | APC only |
| Multi-host | Native (upsd + upsmon) | Limited (apcupsd network mode is clunky) |
| Driver model | Pluggable, maintained upstream | Monolithic |
| Home Assistant | Native integration | Community workaround |
| Config complexity | Higher | Lower |
| Community | Active | Quiet but stable |
If you bought a CyberPower or Eaton because the APC was $40 more at Best Buy, NUT is your only real option anyway. For pure APC shops, apcupsd gets the job done with less ceremony. But for a heterogeneous home lab, NUT wins by default.
The Shutdown Sequence: Don’t Wing This
The whole point of this exercise is a clean shutdown when power fails. Map it out explicitly:
- UPS goes on battery →
upsddetects state change withinPOLLINTERVALseconds upsmonon all hosts receivesOB(on battery) notification- Battery drains to threshold →
upsdreportsLB(low battery) - Slave hosts receive
OB LB→ executeSHUTDOWNCMDimmediately - Master waits for slaves to acknowledge shutdown (up to
HOSTSYNCseconds) - Master executes
SHUTDOWNCMD - NUT sends
poweroffcommand to UPS (if supported) so it cuts power cleanly and can restart when utility power returns
Test this before you need it. Seriously. Pull the plug during a maintenance window, watch the logs, confirm every host shuts down in the right order. A UPS shutdown sequence you’ve never tested is a false sense of security wearing a power brick costume.
# Watch upsmon in real timesudo journalctl -u nut-monitor -f
# Manually check UPS stateupsc myups@localhost ups.statusupsc myups@localhost battery.runtimeupsc myups@localhost battery.chargeThe battery.runtime value is your countdown clock. Set LOWBATT threshold high enough that your slowest host can complete a clean shutdown within that window. Default is usually 300 seconds — if your Proxmox host with 20 VMs needs 4 minutes to gracefully shut down, that’s cutting it close.
The Bottom Line
A UPS without software watching it is just an expensive extension cord with a battery. NUT turns it into an actual protection system — one that coordinates across your whole lab, exposes metrics, integrates with HA and Grafana, and handles the thing you actually care about: making sure your data isn’t mid-write when the lights go out.
Set it up once. Test it. Then forget about it until you get the notification at 2 AM that power just blipped and everything handled it gracefully while you stayed in bed.
That’s the goal. Boring infrastructure that works.