NUT (Network UPS Tools) in Docker with built-in Prometheus exporter. Drop-in replacement for instantlinux/nut-upsd.
Runs a Go entrypoint that handles config generation, process supervision, and metrics export on :9550.
mkdir -p .secrets
echo "your-password" > .secrets/nut-password
docker compose up -dThere are three ways to configure NUT — pick whichever fits your setup.
Environment variables are the simplest. Define UPS devices with NUT_UPS_<n>_*:
environment:
NUT_UPS_1_NAME: ecoflow
NUT_UPS_1_DRIVER: usbhid-ups
NUT_UPS_1_PORT: auto
NUT_UPS_1_DESC: "EcoFlow Delta"
NUT_USER: admin # single-user mode (legacy)
NUT_MAXAGE: "25"Mounting config files gives full control. Mount to /etc/nut/local/:
volumes:
- ./configs/ups.conf:/etc/nut/local/ups.conf:ro
- ./configs/upsd.conf:/etc/nut/local/upsd.conf:ro
- ./configs/upsd.users:/etc/nut/local/upsd.users:ro
- ./configs/upsmon.conf:/etc/nut/local/upsmon.conf:roTemplates are in configs/*.example.
Both together works too — mounted files act as the base, env vars override specific values.
Just increment the index:
environment:
NUT_UPS_1_NAME: ecoflow
NUT_UPS_1_DRIVER: usbhid-ups
NUT_UPS_1_PORT: auto
NUT_UPS_2_NAME: server-ups
NUT_UPS_2_DRIVER: snmp-ups
NUT_UPS_2_PORT: 192.168.1.100Define users with NUT_USER_<n>_* for fine-grained NUT access control:
environment:
NUT_USER_1_NAME: monitor
NUT_USER_1_UPSMON: primary
NUT_USER_2_NAME: admin
NUT_USER_2_ACTIONS: SET,FSD
NUT_USER_2_INSTCMDS: ALL
NUT_USER_3_NAME: remote
NUT_USER_3_UPSMON: secondary
secrets:
- nut-user-1-password
- nut-user-2-password
- nut-user-3-passwordWhen NUT_USER_<n>_* vars are set, the legacy NUT_USER/NUT_PASSWORD/NUT_SERVER vars are ignored.
Passwords are resolved per user: Docker secret NUT_USER_<n>_SECRET_NAME (or convention nut-user-<n>-password) → NUT_USER_<n>_PASSWORD env var → error. No default password in multi-user mode.
UPS devices (<n> = 1, 2, 3, ...):
| Variable | Default | Description |
|---|---|---|
NUT_UPS_<n>_NAME |
ups |
UPS name |
NUT_UPS_<n>_DRIVER |
usbhid-ups |
NUT driver |
NUT_UPS_<n>_PORT |
auto |
Device path or network address |
NUT_UPS_<n>_DESC |
UPS |
Description |
NUT_UPS_<n>_SERIAL |
— | Serial number |
NUT_UPS_<n>_VENDORID |
— | USB vendor ID |
NUT_UPS_<n>_POLLINTERVAL |
— | Poll interval (seconds) |
NUT_UPS_<n>_SDORDER |
— | Shutdown order |
NUT_UPS_<n>_EXTRA |
— | Extra driver options (key=val,key=val) |
Users (<n> = 1, 2, 3, ...):
| Variable | Default | Description |
|---|---|---|
NUT_USER_<n>_NAME |
— | Username (required) |
NUT_USER_<n>_PASSWORD |
— | Password (fallback if no Docker secret) |
NUT_USER_<n>_SECRET_NAME |
nut-user-<n>-password |
Docker secret name |
NUT_USER_<n>_UPSMON |
— | primary or secondary |
NUT_USER_<n>_ACTIONS |
— | SET, FSD, or SET,FSD |
NUT_USER_<n>_INSTCMDS |
— | ALL or comma-separated commands |
Legacy single-user (used when no NUT_USER_<n>_* vars are set):
| Variable | Default | Description |
|---|---|---|
NUT_USER |
admin |
API username |
NUT_PASSWORD |
— | Password (prefer Docker secret) |
NUT_SECRET_NAME |
nut-password |
Docker secret name |
NUT_SERVER |
primary |
primary or secondary |
General:
| Variable | Default | Description |
|---|---|---|
NUT_LISTEN |
0.0.0.0 |
Listen address |
NUT_MAXAGE |
15 |
Max driver age (seconds) |
Scrape :9550/metrics:
scrape_configs:
- job_name: mjolnir
static_configs:
- targets: ['mjolnir:9550']Exported metrics:
| Metric | Type |
|---|---|
mjolnir_ups_status{flag} |
UPS status flags: 1=active, 0=inactive |
mjolnir_device_info{model,mfr,serial,type} |
Device info (value=1) |
mjolnir_scrape_error |
1 if last scrape failed |
mjolnir_<variable> |
Dynamic gauges for all numeric NUT variables |
mjolnir_ups_test_result{result} |
Self-test result enum (OK, Failed, ...) |
mjolnir_battery_charger_status{status} |
Charger status enum |
mjolnir_ups_beeper_status{status} |
Beeper status enum |
mjolnir_input_sensitivity{sensitivity} |
Input sensitivity enum |
mjolnir_input_transfer_reason{reason} |
Transfer reason enum |
mjolnir_input_voltage_status{status} |
Input voltage status enum |
mjolnir_input_current_status{status} |
Input current status enum |
mjolnir_input_frequency_status{status} |
Input frequency status enum |
mjolnir_ups_test_date_seconds |
Last self-test date (Unix timestamp) |
mjolnir_battery_date_seconds |
Battery install date (Unix timestamp) |
mjolnir_battery_mfr_date_seconds |
Battery manufacture date (Unix timestamp) |
mjolnir_ups_firmware_info{version,aux} |
Firmware info (value=1) |
mjolnir_battery_type_info{type} |
Battery type info (value=1) |
mjolnir_ups_type_info{type} |
UPS type info (value=1) |
mjolnir_ups_alarm_active |
1 if alarm active, 0 otherwise |
mjolnir_ups_alarm_info{alarm} |
Alarm text as label (value=1) |
All metrics carry a ups="<name>" label. Enum gauges emit all known values on each scrape (active=1, inactive=0).
| Endpoint | Status |
|---|---|
GET :9550/healthz |
200 if all UPS units respond, 503 otherwise |
GET :9550/readyz |
200 after daemons started, 503 during startup |
GET :9550/metrics |
Prometheus metrics |
GET :9550/diagnostics |
JSON dump of all NUT variables with handling status |
Pass the USB bus instead of running privileged:
devices:
- /dev/bus/usb:/dev/bus/usbdocker exec mjolnir upsc ecoflow@localhost
upsc ecoflow@<host-ip>:3493docker run --rm ghcr.io/st0o0/mjolnir --versionBuilds for linux/amd64 and linux/arm64. Releases are signed with cosign and include SLSA provenance.