UPS and NUT logo

Having a battery backup system (UPS) is essential for home servers hosting multiple services. But it’s not enough for the UPS to supply power for a few minutes: the critical part is that, in case of a prolonged outage, the entire system shuts down in a controlled and orderly manner. In this article I document how I deployed a solution based on NUT on Proxmox to achieve this.

In this example you’ll see that my NUC server is connected via USB cable to the UPS. Some of the services to shut down are critical (one is a VM running my Linux router to the internet, the other provides DNS/DHCP services). What I do is create a dedicated LXC container for NUT (Network UPS Tools), pass the USB device to it, and it takes care of everything, including shutting down its own Proxmox host…

Introduction

I create and configure the new LXC called nut with the following goals:

  • Monitor the status of a USB-connected UPS.
  • Expose the status to Home Assistant (hass), for UPS monitoring.
  • Execute a graceful shutdown of everything running on the NUC (proxmox) in case of prolonged power failure.

Topology example
Topology example.

Proxmox Server

First thing I do is create an LXC container for nut. You can do this from the UI; I downloaded the Debian 12 template. If you prefer the command line:

pct create 123 local:vztmpl/debian-12-standard_12.0-1_amd64.tar.zst \
  -hostname nut \
  -memory 512 \
  -net0 name=eth0,bridge=vmbr0,ip=192.168.1.3/24,gw=192.168.1.1 \
  -rootfs local-lvm:8 \
  -features nesting=1 \
  -unprivileged 1
pct start 123
pct exec 123 -- bash

We find out where the UPS is.

root@proxmox:~# lsusb
:
Bus 001 Device 002: ID 051d:0002 Fry's Electronics MEC0003
------------------     ---------
      |                    |
      |                    +---> We'll use this in /etc/udev/rules.d/50-ups.rules
      |
      +------------------------> We use this in /etc/pve/lxc/123.conf
:

/etc/pve/lxc/123.conf

This file belongs to the LXC container nuc (note that in your case you may have assigned a different number). We need to add the following at the end, assigning the same bus/device IDs (twice) under /dev

lxc.cgroup2.devices.allow: c 189:* rwm
lxc.mount.entry: /dev/bus/usb/001/002 dev/bus/usb/001/002 none bind,optional,create=file

/etc/udev/rules.d/50-ups.rules

We create this file to change the USB permissions to 0666.

The nut container is unprivileged, so its nut user cannot write to the USB port. The trick is to give it write permissions from the host. This is done on the host (proxmox), not in the container, because the container is borrowing hardware from the host. Note the device ID from the lsusb command above: ID 051d:0002.

SUBSYSTEM=="usb", ATTR{idVendor}=="051d", ATTR{idProduct}=="0002", MODE="0666"

I reload the udev rules on Proxmox so they take effect immediately

udevadm control --reload-rules && udevadm trigger

LXC Container nut

I start the new container, log in as root and first update it.

apt update && apt full-upgrade -y

I install nut

apt update && apt full-upgrade -y
apt install nut nut-client -y

I modify /etc/nut/nut.conf:

MODE=standalone

I modify /etc/nut/ups.conf

[ups]
  driver = blazer_usb
  port = auto
  desc = "Fry's Electronics MEC0003"

I modify /etc/nut/upsd.conf

LISTEN 127.0.0.1 3493
LISTEN 192.168.1.3 3493

I modify /etc/nut/upsd.users

[homeassistant]
  password = upspass
  upsmon master
  instcmds = all

I modify /etc/nut/upsmon.conf

MONITOR ups@localhost 1 homeassistant upspass master
MINSUPPLIES 1
SHUTDOWNCMD "/usr/local/sbin/nut-shutdown-master.sh"
POLLFREQ 5
POLLFREQALERT 5
HOSTSYNC 15
DEADTIME 15
POWERDOWNFLAG /etc/killpower
RBWARNTIME 43200
NOCOMMWARNTIME 300
FINALDELAY 5

Permissions in nut

As I mentioned, in unprivileged containers (like this one) running as user nut, both the NUT driver (blazer_usb) and the daemon cannot access the USB device (/dev/bus/usb/...) or the /run directory. To fix this I changed the device permissions to 0666 (on the host) and I’ll create a script to resolve the /run issue.

I create a script, for example: nano /etc/rc.local

#!/bin/sh
# /etc/rc.local file that I'll call from systemd
mkdir -p /run/nut
chown nut:nut /run/nut
# I used to use this but stopped because the port can change
# so I solved it in proxmox with udev rules
#chown nut:nut /dev/bus/usb/001/002
exit 0

I create a custom systemd service to call it at boot: /etc/systemd/system/rc-local.service

# Mini service that runs /etc/rc.local during boot
[Unit]
Description=Run /etc/rc.local
ConditionPathExists=/etc/rc.local
After=network.target

[Service]
Type=forking
ExecStart=/etc/rc.local
TimeoutSec=0
RemainAfterExit=yes
GuessMainPID=no

[Install]
WantedBy=multi-user.target

I set it to start automatically

systemctl daemon-reload
systemctl enable rc-local.service

Starting and testing services

systemctl daemon-reload
systemctl enable nut-server
systemctl enable nut-monitor
systemctl start nut-server
systemctl start nut-monitor

Verify everything works properly

systemctl status nut-server
systemctl status nut-monitor
upsc ups@localhost

Shutdown script in nut

This is the script that nut will execute when it detects a prolonged power failure:

  • Remote shutdown of pihole and router via SSH.
  • Shutdown of hass and wlc VMs from Proxmox (issuing commands via SSH).
  • Shutdown of the Proxmox host.
  • Shutdown of the nut container (poweroff).

Note that nut is the one in charge, it uses IPs to avoid DNS dependency and this script triggers automatically on critical power failure.

View script /usr/local/sbin/nut-shutdown-master.sh


Home Assistant Integration

We connect to the HASS web administration UI

  • Settings > Devices & Services > “Network UPS Tool (NUT)”.
    • I configure the name, IP address of the nut LXC (192.168.1.3), user homeassistant and password upspass

NUT configuration in Devices & Services in HASS
NUT configuration in Devices & Services in HASS.

SSH Configuration:

For nut to access proxmox, router, pihole without a password, I need to properly set up SSH keys. They must trust “nut” to allow it to execute root-level commands (for the graceful shutdown).

On nut:

ssh-keygen -t ed25519 -f /root/.ssh/nut_shutdown_key

Then add the contents of the public key (nut_shutdown_key.pub) to the following files:

  • router (192.168.1.1) -> /root/.ssh/authorized_keys
  • pihole (192.168.1.2) -> /root/.ssh/authorized_keys
  • proxmox (192.168.1.254) -> /etc/pve/priv/authorized_keys

Verify that all three have /etc/ssh/sshd_config properly configured to accept public key authentication. Example (very permissive): allows direct root access, only recommended in trusted environments. In fact, this could be improved by creating users and using sudo; keep in mind this is just an example.

# sshd config that accepts ssh via root.
# first tries public/private key and if it fails asks for password.
Port 22
PubkeyAuthentication yes
PasswordAuthentication yes
PermitRootLogin yes
UsePAM no
AllowAgentForwarding yes
AllowTcpForwarding yes
GatewayPorts yes
X11Forwarding yes
X11DisplayOffset 10
X11UseLocalhost yes
AddressFamily inet
PrintMotd no
PrintLastLog no
Subsystem sftp /usr/lib64/misc/sftp-server
AcceptEnv LANG LC_*

References