cplieger/docker-nut-upsd
NUT UPS daemon with environment-variable-driven configuration
docker-nut-upsd
NUT UPS daemon with environment-variable-driven configuration
Overview
Runs the Network UPS Tools (NUT) upsd daemon in an Alpine container.
The entrypoint script generates ups.conf, upsd.conf, upsd.users, and
upsmon.conf from environment variables at startup. Supports USB HID
UPS devices via device passthrough. Exposes the standard NUT protocol
on port 3493 for network UPS clients.
Important: This container is a NUT server — it monitors the UPS
hardware and serves status data to NUT clients over the network. By
default, SHUTDOWNCMD only logs inside the container. Each host that
needs graceful shutdown should run its own upsmon client pointing at
this server.
Host shutdown support: If you set SHUTDOWN_ON_BATTERY_CRITICAL=true
and mount the host's D-Bus socket (/run/dbus/system_bus_socket), the
container can power off the host via systemd when the UPS reaches
critical battery. This works on any systemd-based host without
switching to a Debian base image.
Custom config override: For advanced users, mount your own NUT
config files as *.user (e.g. ups.conf.user) into /etc/nut/ and
the entrypoint will use them instead of generating from env vars.
Example use case: You have a USB UPS connected to one server and want
other machines on your network to monitor its status. This container
exposes the UPS over NUT's network protocol so any host can run upsmon
or a dashboard like PeaNUT to
track battery level, load, and runtime.
This is an Alpine-based container that runs as root — NUT requires
ownership changes on config files and USB device access at startup.
How It Differs From Network UPS Tools (NUT)
The upstream NUT requires manual
configuration file editing. This image generates all config files from
environment variables, making it fully declarative and Docker-native.
The entrypoint handles NUT's permission requirements and quiet init
flags automatically.
Compared to other NUT Docker images:
- Stays on Alpine (not Debian) — smaller image, same functionality
- Supports host shutdown via D-Bus without installing systemd in the container
- Configurable low-battery and critical-battery thresholds via env vars
- Custom config override via
*.userfile mounts - Configurable upsmon tuning (poll frequency, deadtime, etc.)
- Clean signal handling — SIGTERM gracefully stops all NUT services
Container Registries
This image is published to both GHCR and Docker Hub:
| Registry | Image |
|---|---|
| GHCR | ghcr.io/cplieger/nut-upsd |
| Docker Hub | docker.io/cplieger/nut-upsd |
# Pull from GHCR
docker pull ghcr.io/cplieger/nut-upsd:latest
# Pull from Docker Hub
docker pull cplieger/nut-upsd:latestBoth registries receive identical images and tags. Use whichever you prefer.
Quick Start
services:
nut-upsd:
image: ghcr.io/cplieger/nut-upsd:latest
container_name: nut-upsd
restart: unless-stopped
user: "0:0" # required for config file permissions
mem_limit: 64m
environment:
TZ: "Europe/Paris"
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 from default
ports:
- "3493:3493"
devices:
- /dev/bus/usb:/dev/bus/usb # full bus — survives USB re-enumeration
healthcheck:
test:
- CMD-SHELL
- upsc $$UPS_NAME@127.0.0.1 2>&1 | grep -q 'ups.status' || exit 1
interval: 60s
timeout: 10s
retries: 3
start_period: 30sDeployment
- The compose file maps the entire USB bus (
/dev/bus/usb:/dev/bus/usb)
so the NUT driver can find the UPS regardless of device number changes
after reboots or USB re-enumeration. Verify your UPS is visible:lsusb # Example output: Bus 001 Device 003: ID 0764:0601 Cyber Power System, Inc.Note: Mapping the full bus exposes all USB devices to the container.
The NUT driver only opens the device it recognizes, but if you prefer
tighter isolation you can restrict the mapping to a specific device
(e.g./dev/bus/usb/001/003:/dev/bus/usb/001/003). Keep in mind that
the device number may change after a reboot or USB re-enumeration, so
you would need to update the path when that happens. - Set
UPS_DRIVERto match your UPS model — see the
NUT hardware compatibility list.
Common drivers:usbhid-ups(most USB UPS),
blazer_usb(some Megatec/Q1 protocol UPS). - Change
API_PASSWORDfrom the default. NUT clients on your
network useAPI_USERandAPI_PASSWORDto connect. - Port 3493 is the standard NUT protocol port. Point your NUT
clients (e.g.upsmonon other hosts,
PeaNUT dashboard)
at<host-ip>:3493. - This container runs as root because NUT requires ownership changes on config files and USB devices at startup.
Host shutdown (optional):
To enable automatic host poweroff on battery critical, add these to your
compose file:
environment:
SHUTDOWN_ON_BATTERY_CRITICAL: "true"
volumes:
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socketThis uses D-Bus to call org.freedesktop.login1.Manager.PowerOff on
the host's systemd. Works on any systemd-based Linux distribution.
Custom NUT config (advanced):
Mount your own config files with a .user suffix to bypass env-var
generation:
volumes:
- ./ups.conf:/etc/nut/ups.conf.user:ro
- ./upsd.users:/etc/nut/upsd.users.user:roFor additional configuration options not covered by this image's environment variables, refer to the Network UPS Tools (NUT) documentation.
Environment Variables
| Variable | Description | Default | Required |
|---|---|---|---|
TZ |
Container timezone | Europe/Paris |
No |
UPS_NAME |
NUT UPS identifier used in config files and queries | ups |
No |
UPS_DESC |
Human-readable UPS description shown in NUT clients | My UPS |
No |
UPS_DRIVER |
NUT driver for your UPS model (see NUT hardware compatibility list) | usbhid-ups |
Yes |
UPS_PORT |
UPS device port — use auto for USB auto-detection |
auto |
No |
API_USER |
Username for NUT network clients to authenticate with | monuser |
No |
API_PASSWORD |
Password for the NUT API user — change from default | secret |
Yes |
Additional Environment Variables
The following optional environment variables are also supported but not included in the compose example above. Add them to your environment: block as needed.
Network configuration:
| Variable | Description | Default |
|---|---|---|
API_ADDRESS |
Listen address for upsd | 0.0.0.0 |
API_PORT |
Listen port for upsd | 3493 |
Battery threshold overrides:
Override when the UPS reports low or critical battery. Setting any of
these variables automatically enables ignorelb in ups.conf, telling
NUT to use your thresholds instead of the hardware defaults.
| Variable | Description | Default |
|---|---|---|
LOWBATT_PERCENT |
Low-battery threshold percentage | Hardware default |
LOWBATT_RUNTIME |
Low-battery threshold runtime (seconds) | Hardware default |
CRITBATT_PERCENT |
Critical-battery threshold percentage | Hardware default |
CRITBATT_RUNTIME |
Critical-battery threshold runtime (seconds) | Hardware default |
upsmon tuning:
| Variable | Description | Default |
|---|---|---|
POLLFREQ |
Seconds between UPS status polls | 5 |
POLLFREQALERT |
Seconds between polls when on battery | 5 |
DEADTIME |
Seconds before declaring UPS stale | 15 |
FINALDELAY |
Seconds between shutdown warning and actual shutdown | 5 |
HOSTSYNC |
Seconds to wait for secondary hosts to disconnect | 15 |
NOCOMMWARNTIME |
Seconds before warning about lost UPS communication | 300 |
RBWARNTIME |
Seconds between "replace battery" warnings | 43200 |
Host shutdown:
| Variable | Description | Default |
|---|---|---|
SHUTDOWN_ON_BATTERY_CRITICAL |
Power off host via D-Bus on battery critical | false |
Requires mounting /run/dbus/system_bus_socket from the host. See
Deployment section above for details.
Authentication:
| Variable | Description | Default |
|---|---|---|
ADMIN_PASSWORD |
Password for the NUT admin user (set/FSD actions) | Random |
Ports
| Port | Description |
|---|---|
3493 |
NUT protocol (upsd network clients) |
Docker Healthcheck
The healthcheck queries the UPS status via the NUT protocol to verify
the driver is communicating with the UPS hardware.
When it becomes unhealthy:
- UPS device is disconnected or powered off
- UPS driver failed to start (wrong driver for the hardware)
- upsd daemon is not responding
When it recovers:
- UPS device is reconnected and the driver re-establishes communication. The entire USB bus is mapped, so device number changes after reconnection are handled automatically. A container restart may still be needed for the driver to re-detect the device.
To check health manually:
docker inspect --format='{{json .State.Health.Log}}' nut-upsd | python3 -m json.tool| Type | Command | Meaning |
|---|---|---|
| NUT protocol | upsc $UPS_NAME@127.0.0.1 |
Exit 0 = UPS driver is communicating |
Dependencies
All dependencies are updated automatically via Renovate and pinned by digest or version for reproducibility.
| Dependency | Version | Source |
|---|---|---|
| alpine | 3.23.3 |
Alpine |
Design Principles
- Always up to date: Base images, packages, and libraries are updated automatically via Renovate. Unlike many community Docker images that ship outdated or abandoned dependencies, these images receive continuous updates.
- Minimal attack surface: When possible, pure Go apps use
gcr.io/distroless/static:nonroot(no shell, no package manager, runs as non-root). Apps requiring system packages use Alpine with the minimum necessary privileges. - Digest-pinned: Every
FROMinstruction pins a SHA256 digest. All GitHub Actions are digest-pinned. - Multi-platform: Built for
linux/amd64andlinux/arm64. - Healthchecks: Every container includes a Docker healthcheck.
- Provenance: Build provenance is attested via GitHub Actions, verifiable with
gh attestation verify.
Contributing
Issues, suggestions, and pull requests are welcome.
Credits
This project packages Network UPS Tools (NUT) into a container image. All credit for the core functionality goes to the upstream maintainers.
Disclaimer
These images are built with care and follow security best practices, but they are intended for homelab use. No guarantees of fitness for production environments. Use at your own risk.
This project was built with AI-assisted tooling using Claude Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.
License
This project is licensed under the GNU General Public License v3.0.