A standalone web administration panel for NUT (Network UPS Tools), installable on a Raspberry Pi or any Linux system, with Wake on LAN support and detailed UPS monitoring.
This repository also includes vm/nut-vm.sh, a bash script to automatically create an Ubuntu 24.04 VM on Proxmox VE with NUT pre-configured in netserver mode and the NutWatch web UI installed.
Why a VM instead of LXC? NUT cannot run reliably in LXC containers due to kernel driver detachment restrictions. The VM script creates a lightweight VM specifically for NUT.
- Dashboard — System overview with stat cards (UPS count, users, active services, system health), CPU/memory/disk resource gauges, and quick-lists for all UPS devices and NUT services; one-click system actions (restart NutWatch, reboot, shutdown) with confirmation prompts
- UPS Devices — CRUD management with card-grid view showing real-time telemetry (battery charge bar, load bar, runtime, voltage), per-card driver start/stop/restart, USB scan integration, and recommended config defaults
- UPS Detail Telemetry — Deep-dive view grouped by subsystem (Battery, Input, Output, UPS, Device, Driver) with color-coded charge/load bars, unit-formatted values (V, Hz, W, VA, °C, A), runtime formatting, and raw variable dump; D3 gauge visualizations for battery charge and load
- Historical Data & Charts — Per-UPS time-series data collected every 60 seconds into SQLite, displayed as D3 line charts with selectable ranges (1h, 24h, 7d, 30d) and per-variable filtering; configurable poll interval and 90-day retention
- NUT Users — CRUD for NUT daemon users (upsd.users) with password masking and per-user roles (master/slave/admin); distinct from dashboard Accounts (see below)
- Notifications — Full upsmon.conf editor with monitor line management (add/remove/edit per UPS), global commands (MINSUPPLIES, SHUTDOWNCMD, NOTIFYCMD, POWERDOWNFLAG), timing parameters grid (POLLFREQ, POLLFREQALERT, HOSTSYNC, DEADTIME, etc.), and notification message/flag matrix for all 9 events with SYSLOG/WALL/EXEC/IGNORE checkboxes
- Per-UPS Event Hooks — Fine-grained script hooks per UPS per event (ONLINE, ONBATT, LOWBATT, COMMOK, COMMBAD, SHUTDOWN, REPLBATT, NOCOMM, NOPARENT) with in-browser script editor (Tab support), status badges, and instant save/delete
- Live Log Streaming — Real-time SSE log viewer tailing nut-server, nut-monitor, and nut-driver journals with pause/resume, auto-scroll, color-coded lines (error/warn/info), and configurable recent log loading
- Config Files — Raw in-browser editor for ups.conf, upsd.conf, upsmon.conf, and upsd.users (read-only via this endpoint)
- Wake on LAN — Manage WOL targets (MAC, broadcast, description), create event-to-target mappings for automatic wake on UPS events (ONLINE, ONBATT, etc.), manual "Wake Now" and "Wake All" buttons, and non-destructive auto-dispatch via notifycmd.sh; MAC address field shows a dropdown of hosts discovered from the ARP cache (with hostnames via reverse DNS) and auto-suggests the target name from the hostname
- Service Management — One-click restart (nut-server, nut-monitor, or both) and per-UPS driver start/stop/restart with multi-fallback cleanup (upsdrvctl, systemctl, PID kill, pkill); system-level actions: restart NutWatch (with automatic page reload), reboot, and shutdown
- Accounts & API Keys — Optional session login (admin/viewer roles) for the dashboard and per-user API keys (
Authorization: Bearer <key>) for scripts; a key inherits its owner's role. Fully open until the first admin account is created via the first-run Setup page (or skipped, keeping the app open) ormanage.py create-admin - Atomic Config Writes — All file writes use
tempfile+os.replaceto prevent corruption - Input Validation — Identifier regex, newline injection prevention, type checking on all inputs
- One-Command VM Creation — Downloads Ubuntu 24.04 minimal cloud image (SHA-256 verified), creates a Proxmox VM with EFI boot, virtio-scsi, and QEMU Guest Agent
- Offline Disk Customization — Uses
virt-customizeto install packages, write NUT configs, and set up NutWatch directly into the disk image before VM creation - USB UPS Detection — Scans
lsusboutput, cross-references known vendor IDs (APC051d, CyberPower0764, Eaton0463, Tripp Lite09ae, Liebert10af), handles duplicate models via bus-port notation, and configures USB passthrough - First-Boot Driver Auto-Detection —
nut-detectoneshot systemd service runsnut-scanner -Uon first boot and rewritesups.confwith the detected driver/vendor/product IDs - Cloud-Init — Network configuration, rootfs resize, SSH host keys, and VM password via Proxmox cloud-init
- Guest Agent IP Detection — 5-minute retry loop querying
network-get-interfaces; manual IP entry fallback - Interactive Configuration — Default or Advanced setup modes with whiptail prompts for VM settings (ID, hostname, storage, bridge, RAM, CPU, disk) and NUT settings (UPS name, driver, users, listen address/port)
- Auto-Generated Passwords — Optional secure password generation via
openssl randwith fallback to/dev/urandom - Graceful Failure Handling — NutWatch install failure inside virt-customize is non-fatal; NUT service enablement handles multiple distro systemd variants (nut-driver-enumerator, nut-driver@, nut-driver) with
|| true - Signal Safety —
traphandlers for ERR, EXIT, SIGINT, SIGTERM with cleanup and API status reporting - Resume Support — Partial cloud image downloads use
wget -c - Debian 13 Compatibility — Auto-installs
dhcpcd-basewhen missing for virt-customize network support
notifycmd.sh— Central notify dispatcher that logs all UPS events, executes per-UPS per-event hook scripts from/etc/nut/notify.d/<UPSNAME>_<EVENT>.sh, and then triggers WOL auto-dispatch for any matching event mappings- Hook Samples (
hook-samples/):01-test-marker.sh— Write a marker file when an event fires02-wall-notification.sh— Broadcast awallmessage to all logged-in users03-remote-ssh-shutdown.sh— SSH-shutdown another machine when UPS goes on battery04-webhook-alert.sh— Send Discord/Slack webhook alerts with JSON payload
- GitHub Actions — Lint (shellcheck, shfmt), Python syntax check (py_compile), pytest, frontend tests (Vitest), automated release workflow on tag push, and multi-arch Docker image publishing to GHCR
- Makefile —
check,lint,fmt,fmt-fix,lint-python,test-python,tsc-check,lint-frontend,build-frontend,build-tarball,install-tools build-tarball— Createsnutwatch.tar.gzfor release distribution (git-ignored)- Backend Tests — Pytest suite covering parsers, service layer (UPS, users, upsmon, hooks, WOL, system), auth, route handlers, and utility functions
- Frontend Tests — Vitest suite covering components (Badge, ConfirmDialog, Dashboard, ErrorBoundary, Gauge, HistoryChart, Modal, RestartPromptModal, Skeleton, theme, UpsCard, UpsDetail, WakeOnLan) and utilities (alerts, api, directives, format, logs, service)
- TypeScript — Strict mode (
strict: true,noUnusedLocals,noUnusedParameters) with ESLint flat config;tsc --noEmitandeslint .run as part ofmake check
| Dashboard | UPS Devices | UPS Detail (Info) |
|---|---|---|
![]() |
![]() |
![]() |
| UPS Charts (Input Voltage) | UPS Charts (Runtime) | Hooks |
|---|---|---|
![]() |
![]() |
![]() |
| Users | Notifications | Logs |
|---|---|---|
![]() |
![]() |
![]() |
| Config Files | Wake on LAN | Edit UPS |
|---|---|---|
![]() |
![]() |
![]() |
| Setup (First Admin) | Accounts | Add Account |
|---|---|---|
![]() |
![]() |
![]() |
| Vendor | USB ID | Driver |
|---|---|---|
| APC | 051d |
usbhid-ups |
| CyberPower | 0764 |
usbhid-ups |
| Eaton | 0463 |
usbhid-ups |
| Tripp Lite | 09ae |
usbhid-ups |
| Liebert | 10af |
usbhid-ups |
Other USB UPS devices can be configured manually.
Proxmox Host
├── USB UPS Device
│ └── USB Passthrough ──┐
│ ▼
├── vm/nut-vm.sh VM (Ubuntu 24.04 minimal)
│ ├── Downloads │ ├── NUT Server
│ ├── virt-customize ──►│ │ ├── nut-driver (usbhid-ups)
│ ├── Creates VM │ │ ├── upsd (port 3493)
│ ├── Detects UPS │ │ ├── upsmon (with notifycmd hooks + WOL dispatch)
│ └── Configures │ ├── NutWatch (port 8081)
│ (offline disk │ └── cloud-init (network, resize)
│ modification) │
└── First boot: nut-detect scans USB,
auto-configures driver in ups.conf
src/backend/
├── app.py # Flask application factory & entry point
├── auth.py # Principal resolution (session cookie / Bearer key) + role checks
├── manage.py # CLI: create-admin, reset-password, list-accounts (bootstrap/recovery)
├── config.py # Constants (NUT_DIR, regex, env vars)
├── utils.py # Helpers (atomic write, run_cmd, upsc queries, driver stop)
├── parsers/ # Config file parsers (parse + serialize roundtrip)
│ ├── ups_conf.py
│ ├── upsd_users.py
│ ├── upsmon_conf.py
│ ├── monitor.py # MONITOR line manipulation, MINSUPPLIES
│ └── nut_scanner.py # nut-scanner -U output parser
├── services/ # Business logic layer
│ ├── ups.py # UPS CRUD, auto-add to upsmon, scan
│ ├── users.py # User CRUD with password masking
│ ├── upsmon.py # Full upsmon.conf read/write with validation
│ ├── hooks.py # Per-UPS event hook file management
│ ├── system.py # Service/driver restart, config file raw I/O
│ ├── wol.py # WOL target/event registry, magic packet dispatch
│ ├── history.py # SQLite collection, retention, and range queries
│ ├── resources.py # CPU, memory, and disk monitoring via psutil
│ └── auth_db.py # Accounts + API keys SQLite store (auth.db)
├── routes/ # Flask blueprints (API endpoints)
│ ├── ups.py
│ ├── users.py
│ ├── upsmon.py
│ ├── hooks.py
│ ├── system.py
│ ├── logs.py # SSE log streaming + recent log fetch
│ ├── wol.py # WOL target and event-mapping CRUD endpoints
│ ├── history.py # Historical time-series data endpoints
│ └── auth.py # Setup/login/logout/me, accounts CRUD, API key CRUD
├── static/ # Built React SPA (index.html + assets/)
├── tests/
│ ├── test_parsers.py # Parser roundtrip tests
│ ├── test_auth.py # Principal resolution, role gating tests
│ ├── test_routes.py # API endpoint integration tests
│ ├── test_routes_auth.py # Setup/login/accounts/apikeys route tests
│ ├── test_manage_cli.py # CLI create-admin/reset-password/list-accounts tests
│ ├── test_services_hooks.py # Hook file CRUD tests
│ ├── test_services_system.py # Service/driver/config tests
│ ├── test_services_ups.py # UPS CRUD tests
│ ├── test_services_upsmon.py # Upsmon config tests
│ ├── test_services_users.py # User CRUD tests
│ ├── test_services_wol.py # WOL target/mapping tests
│ ├── test_services_history.py # History collection/query tests
│ ├── test_services_auth_db.py # Account/API key store tests
│ └── test_utils.py # Utility function tests
├── scripts/
│ ├── notifycmd.sh # UPS event notify dispatcher (hooks + WOL)
│ └── nutwatch-wol-dispatch # WOL auto-dispatch called by notifycmd.sh
├── nutwatch.service # systemd unit file
└── requirements.txt # flask, psutil, pytest, wakeonlan
src/frontend/src/
├── App.tsx # Root component with section routing + auth gating
├── api.ts # Fetch wrapper for /api/* (generic typed, 401 handling)
├── AuthProvider.tsx # Session/account context (status, login, logout, setup)
├── types.ts # Shared TypeScript interfaces and types
├── constants/index.ts # Section IDs, API paths, event lists, defaults
├── theme.tsx # Light/dark theme provider
├── styles/
│ ├── base.css # Global resets and base styles
│ ├── components.css # Shared component styles
│ └── variables.css # CSS custom properties (colors, spacing)
├── components/
│ ├── Dashboard.tsx # Stat cards + UPS/services overview, resource gauges, system actions
│ ├── ErrorBoundary.tsx # Error boundary with fallback UI
│ ├── UpsDevices.tsx # UPS card grid + scan/add/edit/delete
│ ├── UpsCard.tsx # Individual UPS card with metrics & actions
│ ├── UpsDetail.tsx # Deep-dive telemetry grouped by subsystem
│ ├── UpsModal.tsx # Add/edit UPS form with recommended defaults
│ ├── Gauge.tsx # D3 gauge for battery charge and load
│ ├── HistoryChart.tsx # D3 line charts with time-range selector
│ ├── Users.tsx # User table with CRUD
│ ├── UserModal.tsx # Add/edit user form
│ ├── Notifications.tsx # Full upsmon.conf editor (monitors, messages, flags, timing)
│ ├── RestartPromptModal.tsx # Restart prompt after config saves
│ ├── HooksSection.tsx # Per-UPS event hook table
│ ├── HookEditor.tsx # In-browser script editor with Tab support
│ ├── Logs.tsx # Live SSE log viewer with pause/auto-scroll
│ ├── WakeOnLan.tsx # WOL target registry + event mapping management
│ ├── ConfigFiles.tsx # Raw config file editor
│ ├── ServiceStatus.tsx # Service active/inline status bar
│ ├── Skeleton.tsx # Loading skeleton placeholder
│ ├── Sidebar.tsx # Navigation sidebar
│ ├── Badge.tsx # Status badge (online/onbatt/offline/unknown)
│ ├── Modal.tsx # Reusable modal dialog system
│ ├── ConfirmDialog.tsx # Confirm/alert/dangerConfirm dialog system
│ ├── ThemeSettings.tsx # Theme toggle UI
│ ├── Setup.tsx # First-run admin creation (with Skip)
│ ├── Login.tsx # Session login form
│ ├── ApiKeys.tsx # Self-service API key list/create/revoke
│ ├── ApiKeyModal.tsx # Create-key modal (shows raw key once)
│ ├── Accounts.tsx # Admin-only account table
│ └── AccountModal.tsx # Add/edit account form with role selector
└── utils/
├── alerts.ts # System alert/resource status helpers
├── directives.ts # Key=value directive parsing/formatting
├── format.ts # Runtime seconds → "Xh Ym" formatter
├── logs.ts # Log line color classification
├── metrics.ts # UPS variable selection and chart data helpers
└── service.ts # Service status → badge class mapping
| Method | Path | Description |
|---|---|---|
GET |
/api/ups |
List all UPS devices with status |
POST |
/api/ups |
Add a new UPS (auto-adds to upsmon.conf) |
GET |
/api/ups/<name> |
Get single UPS config |
GET |
/api/ups/<name>/detail |
Get live telemetry via upsc |
PUT |
/api/ups/<name> |
Update UPS config |
DELETE |
/api/ups/<name> |
Delete UPS (stops driver, cleans hooks) |
POST |
/api/ups/scan |
Run nut-scanner -U to detect USB UPS devices |
| Method | Path | Description |
|---|---|---|
GET |
/api/users |
List all users (passwords masked) |
POST |
/api/users |
Add a new user |
PUT |
/api/users/<name> |
Update user |
DELETE |
/api/users/<name> |
Delete user |
| Method | Path | Description |
|---|---|---|
GET |
/api/upsmon/config |
Read full upsmon.conf |
PUT |
/api/upsmon/config |
Write upsmon.conf (with validation) |
| Method | Path | Description |
|---|---|---|
GET |
/api/hooks/<upsname> |
List existing hooks for a UPS |
GET |
/api/hooks/<upsname>/<event> |
Get hook script content |
PUT |
/api/hooks/<upsname>/<event> |
Create/update hook script |
DELETE |
/api/hooks/<upsname>/<event> |
Delete hook script |
| Method | Path | Description |
|---|---|---|
POST |
/api/service/restart-server |
Restart nut-server |
POST |
/api/service/restart-monitor |
Restart nut-monitor |
POST |
/api/service/restart-all |
Restart both services |
POST |
/api/service/status |
Combined nut-server + nut-monitor status |
GET |
/api/service/status-detailed |
Per-service active state (nut-driver, nut-server, nut-monitor) |
POST |
/api/driver/<name>/start |
Start UPS driver |
POST |
/api/driver/<name>/stop |
Stop UPS driver (multi-fallback cleanup) |
POST |
/api/driver/<name>/restart |
Restart UPS driver |
| Method | Path | Description |
|---|---|---|
GET |
/api/system/resources |
CPU%, memory usage/GB, and disk usage/GB |
POST |
/api/system/restart-nutwatch |
Restart the NutWatch web service (page reloads automatically) |
POST |
/api/system/reboot |
Reboot the entire system (requires an admin account to be configured) |
POST |
/api/system/shutdown |
Shut down the system (requires an admin account to be configured) |
| Method | Path | Access | Description |
|---|---|---|---|
GET |
/api/auth/status |
public | Bootstrap/authentication state |
POST |
/api/auth/setup |
first-run only | Create the first admin account (blocked once any account exists) |
POST |
/api/auth/login |
public | Username/password → session cookie (rate-limited) |
POST |
/api/auth/logout |
session | Clear session |
GET |
/api/auth/me |
session or key | Current identity + role |
GET |
/api/accounts |
admin | List accounts |
POST |
/api/accounts |
admin | Create an account |
PUT |
/api/accounts/<id> |
admin | Edit role/password/active status |
DELETE |
/api/accounts/<id> |
admin | Deactivate an account (soft delete) |
GET |
/api/apikeys |
any account | List the caller's own API keys (metadata only) |
POST |
/api/apikeys |
any account | Create a key — returns the raw key once |
DELETE |
/api/apikeys/<id> |
owner | Revoke one of the caller's own keys |
| Method | Path | Description |
|---|---|---|
GET |
/api/config/<filename> |
Read config file content |
PUT |
/api/config/<filename> |
Write config file (read-only for upsd.users) |
Allowed files: ups.conf, upsd.conf, upsmon.conf, upsd.users
| Method | Path | Description |
|---|---|---|
GET |
/api/logs/recent?lines=N |
Recent N lines from NUT journals |
GET |
/api/logs/stream |
SSE stream tailing nut-server + nut-monitor + nut-driver |
| Method | Path | Description |
|---|---|---|
GET |
/api/history/<ups>?range=<range>&variables=<vars> |
Time-series snapshots (ranges: 1h, 24h, 7d, 30d) |
GET |
/api/history/<ups>/variables |
Available tracked variables for a UPS |
| Method | Path | Description |
|---|---|---|
GET |
/api/wol/targets |
List all WOL targets |
POST |
/api/wol/targets |
Create a WOL target |
PUT |
/api/wol/targets/<name> |
Update a WOL target |
DELETE |
/api/wol/targets/<name> |
Delete a WOL target |
POST |
/api/wol/targets/<name>/wake |
Send magic packet to target |
POST |
/api/wol/wake-all |
Send magic packet to all targets |
GET |
/api/wol/mappings |
List all event mappings |
POST |
/api/wol/mappings |
Create an event mapping |
DELETE |
/api/wol/mappings/<id> |
Delete an event mapping |
GET |
/api/wol/network-hosts |
List ARP-cache hosts (IP, MAC, hostname) for MAC suggestions |
Installs and configures NUT in netserver mode and the NutWatch web UI in one step:
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/JuanCF/nutwatch/main/scripts/setup.sh)"Non-interactive mode with auto-generated passwords:
curl -fsSL https://raw.githubusercontent.com/JuanCF/nutwatch/main/scripts/setup.sh | sudo AUTO=1 bashNutWatch-only mode — Use --install-only on a machine that already has NUT configured:
sudo bash scripts/setup.sh --install-onlyUpdate mode — Use --update to upgrade only the NutWatch application code in an existing installation (VM or standalone), preserving NUT configs, hooks, and the Python venv:
sudo bash scripts/setup.sh --updateOverride any setting via environment variables:
sudo NUT_UPS_NAME="myups" NUT_ADMIN_PASS="securepass" AUTO=1 bash scripts/setup.shWhat it does (fresh install):
- Installs
nut-server,nut-client,usbutils - Writes all NUT config files (nut.conf, ups.conf, upsd.conf, upsd.users, upsmon.conf)
- Scans USB for UPS devices (first-boot auto-detection service available)
- Installs
notifycmd.shwith per-UPS per-event hook support and WOL auto-dispatch - Installs
nutwatch-wol-dispatchto/usr/local/bin/for event-driven WOL - Installs NutWatch web UI on port 8081 with systemd service
- Enables and starts all NUT services
- Configures firewall (ufw) rules
| Environment Variable | Default | Description |
|---|---|---|
AUTO |
(unset) | Set to 1 for non-interactive mode with defaults |
NUT_UPS_NAME |
ups |
UPS identifier |
NUT_UPS_DESC |
My UPS |
UPS description |
NUT_DRIVER |
usbhid-ups |
NUT driver |
NUT_ADMIN_USER |
admin |
NUT daemon admin username |
NUT_ADMIN_PASS |
(auto-gen) | NUT daemon admin password |
NUT_MONITOR_USER |
monuser |
NUT monitor username |
NUT_MONITOR_PASS |
(auto-gen) | NUT monitor password |
NUT_LISTEN_ADDR |
0.0.0.0 |
NUT listen address |
NUT_LISTEN_PORT |
3493 |
NUT listen port |
NUTWATCH_REF |
v1.3.0 |
NutWatch release tag |
NUTWATCH_URL_PREFIX |
(unset) | Override tarball URL for local testing |
After install, the app is fully open (no login). Create the first admin with manage.py create-admin <username> (run from the NutWatch install directory, e.g. venv/bin/python manage.py create-admin admin) to turn on session login and per-user API keys — or create it from the dashboard's first-run Setup page.
Install just the NutWatch web UI on a machine that already has NUT configured:
curl -fsSL https://raw.githubusercontent.com/JuanCF/nutwatch/main/scripts/setup.sh | sudo bash -s -- --install-onlySet the NUTWATCH_REF env var to pin a specific release version.
Resetting a lost NutWatch account password. If you're locked out of the UI, reset the password from the shell on the machine running NutWatch:
sudo /opt/nutwatch/venv/bin/python /opt/nutwatch/manage.py reset-password USERNAMEYou'll be prompted for the new password (or pipe it with --password-stdin
for scripting). list-accounts shows configured usernames; create-admin <username> also resets an existing account's password and guarantees the
admin role.
Prebuilt multi-arch images (linux/amd64, linux/arm64) are published to the
GitHub Container Registry, so there's no need to clone the repo. The image
bundles NUT, the NutWatch backend, and a built React frontend.
ghcr.io/juancf/nutwatch
| Tag | Points at |
|---|---|
latest |
Most recent release |
1.3.0, 1.3, 1 |
Specific release / minor / major line |
main |
Rolling build of the main branch (unreleased) |
# Run with Docker Compose — grab just the compose file, no clone required
curl -fsSLO https://raw.githubusercontent.com/JuanCF/nutwatch/main/docker-compose.yml
docker compose up -d
# Or run the image directly
docker run -d \
--name nutwatch \
--device /dev/bus/usb:/dev/bus/usb \
--device-cgroup-rule 'c 189:* rwm' \
-p 8081:8081 \
-p 3493:3493 \
-v nutwatch-config:/etc/nut \
-v nutwatch-data:/var/lib/nutwatch \
ghcr.io/juancf/nutwatch:latestTo upgrade, pull the new image and recreate the container — both volumes are preserved:
docker compose pull && docker compose up -dBuilding from source instead. A multi-stage Dockerfile is included; clone
the repo, uncomment the build: . line in docker-compose.yml, and run
docker compose up -d --build (or docker build -t nutwatch .).
USB access. NUT drivers need to talk to the UPS over USB. The container gets the host USB bus plus an allow-rule for USB character devices:
docker run -d \
--device /dev/bus/usb:/dev/bus/usb \
--device-cgroup-rule 'c 189:* rwm' \
... nutwatchThe cgroup rule (major number 189 is USB) is evaluated when devices appear,
so UPS hotplug works without privileged mode. If USB access still fails on
your setup, --privileged (or privileged: true in compose) is the
documented fallback.
Credentials. On first start the entrypoint generates random passwords for
the NUT admin and monuser users and writes them to /etc/nut/upsd.users
inside the nutwatch-config volume; the Flask session key is generated by the
backend and persisted in the auth database inside nutwatch-data. Generated
values are not printed to logs. To use your own credentials, set
NUT_ADMIN_PASS / NUT_MONITOR_PASS (and optionally NUT_ADMIN_USER /
NUT_MONITOR_USER or NUTWATCH_SECRET_KEY, at least 32 characters) before
the first start. Because upsd.users is only initialized on first start,
rotate existing credentials via the NutWatch UI (NUT Users tab, which
restarts NUT automatically) or the Config Files tab instead of environment
variables.
Password recovery. Locked out of the UI? Reset a NutWatch account password from inside the container:
docker exec -it nutwatch /opt/nutwatch/venv/bin/python /opt/nutwatch/manage.py reset-password USERNAMEThe same CLI handles bootstrap and listing: list-accounts shows usernames,
create-admin <username> promotes an existing account and resets its
password.
Persistent data. Two volumes are used:
/etc/nut— NUT configuration files (ups.conf,upsd.users,upsmon.conf, hooks innotify.d/, etc.)/var/lib/nutwatch— NutWatch account/API-key database and UPS history SQLite database
Container notes.
- The container uses
supervisordinstead ofsystemd. A smallsystemctlshim maps the UI's service-restart actions tosupervisorctlso NUT service restarts still work. - System-level actions (reboot/shutdown) are disabled inside the container.
- Live log streaming relies on
journalctl; in the container this is not available. Usedocker exec nutwatch tail -f /var/log/supervisor/upsd.logor inspect the individual supervisor log files instead. - Wake on LAN: magic packets default to the
255.255.255.255broadcast, which does not leave Docker's default bridge network. For WOL to reach hosts on your LAN, either run the container with--network host(network_mode: hostin compose) or set a directed broadcast address (e.g.192.168.1.255) on each WOL target.
bash -c "$(curl -fsSL https://raw.githubusercontent.com/JuanCF/nutwatch/main/vm/nut-vm.sh)"# Download the script
curl -fsSL https://raw.githubusercontent.com/JuanCF/nutwatch/main/vm/nut-vm.sh -o nut-vm.sh
# Or clone the repository
git clone https://github.com/JuanCF/nutwatch.git
cd nutwatch
# Run from source
bash vm/nut-vm.sh# Run on Proxmox host as root (from cloned repo)
bash vm/nut-vm.sh
# Or if downloaded directly
bash nut-vm.sh| Variable | Default | Description |
|---|---|---|
VERBOSE |
(unset) | Set to yes to show full command output |
NUTWATCH_URL_PREFIX |
(unset) | Override the GitHub Releases URL for the nutwatch tarball |
COMMUNITY_SCRIPTS_URL |
https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main |
Base URL for sourcing helper functions |
VERBOSE=yes bash vm/nut-vm.sh # Verbose output
NUTWATCH_URL_PREFIX=https://example.com/my-fork bash vm/nut-vm.sh
COMMUNITY_SCRIPTS_URL=https://my-mirror.example.com bash vm/nut-vm.sh| Variable | Default | Description |
|---|---|---|
NUTWATCH_HOST |
0.0.0.0 |
Listen address for the web server |
NUTWATCH_PORT |
8081 |
Listen port for the web server |
NUTWATCH_HISTORY_INTERVAL |
60 |
Seconds between UPS variable snapshots |
NUTWATCH_HISTORY_RETENTION_DAYS |
90 |
Days of history to retain in SQLite |
NUTWATCH_AUTH_DB |
/var/lib/nutwatch/auth.db |
SQLite path for accounts + API keys |
NUTWATCH_SECRET_KEY |
(auto-generated) | Flask session signing key; auto-generated and persisted in the auth DB on first run if unset |
NUTWATCH_SESSION_COOKIE_SECURE |
false |
Set to true to mark the session cookie Secure (only if serving over HTTPS — plain-HTTP LAN deployments must leave this false) |
| Variable | Default | Description |
|---|---|---|
NUTWATCH_REF |
v1.3.0 |
Git tag for release download URL |
NUTWATCH_URL_PREFIX |
(unset) | Override URL for testing local builds |
- VM Configuration — VM ID, hostname, storage pool, network bridge, RAM, CPU cores, disk size, VM username/password
- UPS Detection — Auto-scan for USB UPS devices, duplicate model handling, manual entry fallback
- NUT Configuration — UPS name/description, driver, admin/monitor users, listen address/port
git tag v1.2.3
git push origin v1.2.3The GitHub Actions workflow will run lint checks, build nutwatch.tar.gz, and create a GitHub Release.
In parallel, .github/workflows/docker-publish.yml builds the linux/amd64 +
linux/arm64 image and pushes it to ghcr.io/juancf/nutwatch as :1.2.3,
:1.2, :1, and :latest. Pushes to main publish a rolling :main tag, and
pull requests touching the Docker or app sources build the image without
pushing. Authentication uses the built-in GITHUB_TOKEN — no secrets to
configure.
make build-tarball
python3 -m http.server 8080 --directory .
NUTWATCH_URL_PREFIX="http://<your-ip>:8080" bash vm/nut-vm.shmake check # Full CI suite: lint + format check + Python lint + pytest + frontend tests
make lint # shellcheck only
make fmt # shfmt -d -i 2 (check only)
make fmt-fix # shfmt -w -i 2 (auto-fix)
make lint-python # py_compile check on all Python files
make test-python # pytest on src/backend/tests/
make test-frontend # Vitest on src/frontend/src/__tests__/
make build-frontend # npm ci + npm run build
make build-tarball # Create nutwatch.tar.gz for distribution
make install-tools # Install dev dependenciesPlace scripts in /etc/nut/notify.d/<UPSNAME>_<EVENT>.sh:
| Sample | Description |
|---|---|
01-test-marker.sh |
Write a marker file to /tmp/ups-test.log |
02-wall-notification.sh |
Broadcast wall message and syslog alert |
03-remote-ssh-shutdown.sh |
SSH into another machine and shut it down |
04-webhook-alert.sh |
Send Discord/Slack webhook JSON payload |
Each hook receives $UPSNAME and $NOTIFYTYPE environment variables from upsmon.
# Check VM status
qm list
# Verify USB passthrough
qm config <vmid> | grep usb
# Test NUT from Proxmox host
upsc ups@<VM_IP>
# Test from another machine
upsc ups@<VM_IP>:3493
# Check NUT services inside VM
systemctl status nut-server nut-monitor
# Access NutWatch web UI
http://<VM_IP>:8081- NUT passwords should be strong and unique
- The netserver listens on all interfaces by default (
0.0.0.0) - Consider firewall rules to restrict NUT port (3493) access
- The VM password is set via Proxmox's built-in cloud-init (
qm set --cipassword) - Hook scripts are owned
root:nutwith750permissions for secure upsmon execution - WOL registry files (
wol.json,wol-events.json) are ownedroot:nutwith640permissions - Config file writes use atomic
tempfile+os.replaceto prevent partial writes
MIT License — See LICENSE file for details.
Contributions welcome! Please feel free to submit a Pull Request.
- Network UPS Tools project
- Proxmox VE community
- Ubuntu Cloud Images
- Open an issue
- NUT Users Mailing List: https://alioth-lists.debian.net/lists/lists.alioth.debian.net
Disclaimer: This script modifies your Proxmox configuration. Always review scripts before running them as root. Test in a non-production environment first.














