Monitor your UPS and let networked machines shut down gracefully during power outages
7.9K
Monitor your UPS and let networked machines shut down gracefully during power outages.
Monitors your UPS (uninterruptible power supply) and exposes its status over the network so other machines can shut down gracefully during a power outage.
The container runs the Network UPS Tools (NUT) upsd daemon in Alpine Linux. The entrypoint script generates all NUT configuration files (ups.conf, upsd.conf, upsd.users, upsmon.conf) from environment variables at startup.
/etc/nut/upsd.pem; legacy cleartext clients keep working (see TLS)SHUTDOWN_ON_BATTERY_CRITICAL=true)/etc/nut/{ups.conf,upsd.conf,upsd.users,upsmon.conf}.user to bypass env-var generation. You then own every directive in that file, including the ones the rest of the image reads:
ups.conf.user: keep the section name ([...]) equal to UPS_NAME and its driver directive equal to UPS_DRIVER. The healthcheck, generated upsmon.conf MONITOR line, and comms watchdog use the section name. NUT names the PID file from the driver directive and section, while the startup gate waits for /var/run/nut/$UPS_DRIVER-$UPS_NAME.pid. Either mismatch is fatal at boot: the container logs UPS driver did not confirm a live PID for the expected binary in time and exits about five seconds in, so your restart policy brings it straight back to the same state. A mounted ups.conf.user also makes LOWBATT_PERCENT and LOWBATT_RUNTIME inertups.conf.user: on a serial Modbus driver (apc_modbus, generic_modbus, adelsystem_cbi), keep the baud rate to one libmodbus's classic _get_termios_speed table maps — every standard POSIX B* rate is there, 110 through 115200 and beyond (9600 is each of those drivers' own default), but a non-standard rate such as 14400 or 28800 is not. libmodbus is built here without termios2 support, which does not compile on Alpine/musl, so baudrate 14400 or ser_baud_rate 28800 is accepted, silently opened at 9600, and the driver never reaches the device: what you see is a comms failure, with nothing naming the baud rateups.conf.user: keep the driver's worst-case start inside 90s, the outer bound the entrypoint puts on upsdrvctl start; NUT's own maxstartdelay defaults to 75s per driver and maxretry to 1 attempt (ups.conf), so raising either — maxretry 2 alone allows up to 75 + 5 + 75 = 155s — can push a configuration NUT considers healthy past that bound, and the container logs upsdrvctl start failed or timed out at boot and exits, leaving your restart policy to loop itupsd.conf.user: keep LISTEN on API_ADDRESS and API_PORT, where those same probes look; a divergent LISTEN fails every one of them against a correctly-serving upsd, the container exits after about a minute of failed probes, and your restart policy brings it back into the same stateupsmon.conf.user: keep a SHUTDOWNCMD line, or a forced shutdown takes no action on the host even with SHUTDOWN_ON_BATTERY_CRITICAL=true; upsmon prints Warning: no shutdown command defined! once at startupupsmon.conf.user: keep POWERDOWNFLAG /var/run/nut-secrets/killpower at that exact path, or the comms watchdog cannot stand down during a real host poweroff. A driver bounce causes a bounded FSD blackout for networked secondaries and skips the HOSTSYNC wait because NUT v2.8.5 reads the secondary-login count as zero after the upsd-side failure. The boot-time stale-flag clear also stops matchingupsmon.conf.user: keep the NOTIFYCMD line and the EXEC notify flags (see Alerting)docker pull cplieger/docker-nut-upsd:latest
Also published to ghcr.io/cplieger/docker-nut-upsd with identical images and tags. Release versions are tagged vX.Y.Z alongside latest.
# Example compose for docker-nut-upsd. See the README for all configuration options and hardening.
services:
nut-upsd:
image: ghcr.io/cplieger/docker-nut-upsd:latest
container_name: nut-upsd
restart: unless-stopped
environment:
UPS_NAME: "ups"
UPS_DESC: "My UPS"
UPS_DRIVER: "usbhid-ups" # see NUT hardware compatibility list
UPS_PORT: "auto" # auto = USB auto-detection
API_USER: "monuser"
API_PASSWORD: "secret" # change this
ports:
- "3493:3493"
# USB hotplug: bind the bus LIVE (not via devices:) plus the USB-major cgroup rule, so a
# UPS that re-enumerates to a new node stays reachable without a recreate. See README.
device_cgroup_rules:
- "c 189:* rmw"
volumes:
- "/dev/bus/usb:/dev/bus/usb"
Apache-2.0. See LICENSE.
The image carries the license text of every bundled component under
/usr/share/licenses/. The Alpine packages in the image ship no license file
upstream, so their license texts are kept under licenses/ in this repository
and copied in.
The image packages Network UPS Tools
(GPL-2.0-or-later), compiled from the release tarball the Dockerfile fetches at
the version its NUT_VERSION argument pins
(https://github.com/networkupstools/nut/releases/download/<version>/nut-<version>.tar.gz),
and links libmodbus and
Net-SNMP, each compiled the same way
from the tarball its own version argument pins. Each component's own license
text is in that tree.
The build applies checked-in backports of upstream NUT source, so those files stay GPL-2.0-or-later:
patches/cve-2026-54161-notifycmd-execvp.patch backports upstream commit
ecf98e7542e4ae2b62b211622ee26989274b2220.patches/libusb-exit-reconnect-deadlock.patch backports upstream commit
bfbba15928aa6a91b3e4b8943e0cad16199d9d48.patches/libusb-rdlens-oob-read.patch backports upstream commit
edc06fb39435b892d5daeec53cb4845cb12d1d50.patches/richcomm-libusb-context-reopen.patch backports upstream commit
ce2364e2b1e79406248be50b01a031db67c1c9fd.This repository's Dockerfile and those patch files are the complete recipe for
the NUT, libmodbus and Net-SNMP binaries the image ships: the corresponding
source is the upstream tarball each pin names, with the patches above applied.
Content type
Image
Digest
sha256:c4c81469e…
Size
25.1 MB
Last updated
about 4 hours ago
docker pull cplieger/docker-nut-upsd