{"id":46270031,"url":"https://github.com/cplieger/docker-nut-upsd","last_synced_at":"2026-05-30T01:04:54.812Z","repository":{"id":342042769,"uuid":"1172566487","full_name":"cplieger/docker-nut-upsd","owner":"cplieger","description":"NUT UPS daemon with environment-variable-driven configuration","archived":false,"fork":false,"pushed_at":"2026-05-22T13:16:36.000Z","size":66,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T13:28:04.575Z","etag":null,"topics":["alpine","battery","dbus","docker","homelab","monitoring","network-ups-tools","nut","power-management","ups","upsd","usb-hid"],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cplieger.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-04T13:05:45.000Z","updated_at":"2026-05-22T13:10:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cplieger/docker-nut-upsd","commit_stats":null,"previous_names":["cplieger/docker-nut-upsd"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/cplieger/docker-nut-upsd","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fdocker-nut-upsd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fdocker-nut-upsd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fdocker-nut-upsd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fdocker-nut-upsd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cplieger","download_url":"https://codeload.github.com/cplieger/docker-nut-upsd/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fdocker-nut-upsd/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33676192,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alpine","battery","dbus","docker","homelab","monitoring","network-ups-tools","nut","power-management","ups","upsd","usb-hid"],"created_at":"2026-03-04T03:02:49.490Z","updated_at":"2026-05-30T01:04:54.807Z","avatar_url":"https://github.com/cplieger.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# docker-nut-upsd\n\n![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)\n[![GitHub release](https://img.shields.io/github/v/release/cplieger/docker-nut-upsd)](https://github.com/cplieger/docker-nut-upsd/releases)\n[![Image Size](https://ghcr-badge.egpl.dev/cplieger/nut-upsd/size)](https://github.com/cplieger/docker-nut-upsd/pkgs/container/nut-upsd)\n![Platforms](https://img.shields.io/badge/platforms-amd64%20%7C%20arm64-blue)\n![base: Alpine 3.23.4](https://img.shields.io/badge/base-Alpine_3.23.4-0D597F?logo=alpinelinux)\n\nMonitor your UPS and let networked machines shut down gracefully during power outages.\n\n## What it does\n\nMonitors your UPS (uninterruptible power supply) and exposes its status over the network so other machines can shut down gracefully during a power outage. A UPS is a battery backup that keeps your equipment running when the power goes out — this container watches that battery and tells every machine on your network when it's time to shut down safely.\n\nThe 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.\n\n- Supports USB HID, Modbus, and SNMP UPS devices\n- Exposes the standard NUT protocol on port 3493 for network clients\n- Optional host shutdown via D-Bus when the UPS reaches critical battery (`SHUTDOWN_ON_BATTERY_CRITICAL=true`)\n- Custom config override: mount your own NUT config files as `*.user` (e.g. `ups.conf.user`) into `/etc/nut/` to bypass env-var generation\n- Configurable low-battery and critical-battery thresholds\n- Clean signal handling — SIGTERM gracefully stops all NUT services\n\n### Why this design\n\n- **Environment-variable config** — no need to hand-edit `nut.conf` files; the entrypoint generates them declaratively from env vars\n- **Single container replaces three daemons** — bundles the NUT driver, `upsd`, and `upsmon` so you deploy one service instead of three\n- **Minimal Alpine base** — small image with only the packages NUT needs; no extras that increase attack surface\n- **Compiled from upstream sources** — NUT, libmodbus, and net-snmp built from latest upstream (not distro packages) for zero known CVEs\n\n## Quick start\n\nAvailable from both `ghcr.io/cplieger/nut-upsd` and `docker.io/cplieger/nut-upsd` — identical images and tags.\n\n```yaml\nservices:\n  nut-upsd:\n    image: ghcr.io/cplieger/nut-upsd:latest\n    container_name: nut-upsd\n    restart: unless-stopped\n    user: \"0:0\"  # required for config file permissions\n\n    environment:\n      TZ: \"Europe/Paris\"\n      UPS_NAME: \"ups\"\n      UPS_DESC: \"My UPS\"\n      UPS_DRIVER: \"usbhid-ups\"  # see NUT hardware compatibility list\n      UPS_PORT: \"auto\"  # auto = USB auto-detection\n      API_USER: \"monuser\"\n      API_PASSWORD: \"secret\"  # rotate if your NUT client supports custom credentials\n\n    ports:\n      - \"3493:3493\"\n\n    devices:\n      - /dev/bus/usb:/dev/bus/usb  # full bus — survives USB re-enumeration\n```\n\n## Configuration reference\n\n### Environment variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `TZ` | Container timezone | `Europe/Paris` |\n| `UPS_NAME` | NUT UPS identifier used in config files and queries | `ups` |\n| `UPS_DESC` | Human-readable UPS description shown in NUT clients | `My UPS` |\n| `UPS_DRIVER` | NUT driver for your UPS model (see [NUT HCL](https://networkupstools.org/stable-hcl.html)) | `usbhid-ups` |\n| `UPS_PORT` | UPS device port — use `auto` for USB auto-detection | `auto` |\n| `API_USER` | Username for NUT network clients to authenticate with | `monuser` |\n| `API_PASSWORD` | Password for the NUT API user (entrypoint warns on weak credentials) | `secret` |\n| `API_ADDRESS` | Listen address for upsd | `0.0.0.0` |\n| `API_PORT` | Listen port for upsd | `3493` |\n| `LOWBATT_PERCENT` | Low-battery threshold percentage (enables `ignorelb`) | Hardware default |\n| `LOWBATT_RUNTIME` | Low-battery threshold runtime in seconds (enables `ignorelb`) | Hardware default |\n| `CRITBATT_PERCENT` | Critical-battery threshold percentage (enables `ignorelb`) | Hardware default |\n| `CRITBATT_RUNTIME` | Critical-battery threshold runtime in seconds (enables `ignorelb`) | Hardware default |\n| `POLLFREQ` | Seconds between UPS status polls | `5` |\n| `POLLFREQALERT` | Seconds between polls when on battery | `5` |\n| `DEADTIME` | Seconds before declaring UPS stale | `15` |\n| `FINALDELAY` | Seconds between shutdown warning and actual shutdown | `5` |\n| `HOSTSYNC` | Seconds to wait for secondary hosts to disconnect | `15` |\n| `NOCOMMWARNTIME` | Seconds before warning about lost UPS communication | `300` |\n| `RBWARNTIME` | Seconds between \"replace battery\" warnings | `43200` |\n| `SHUTDOWN_ON_BATTERY_CRITICAL` | Power off host via D-Bus on battery critical | `false` |\n| `ADMIN_PASSWORD` | Password for the NUT admin user (set/FSD actions); auto-generated if unset | Random (cached) |\n\n### Volumes\n\n| Mount | Description |\n|-------|-------------|\n| `/dev/bus/usb` | Full USB bus (device passthrough for UPS hardware) |\n| `/run/dbus/system_bus_socket` | Host D-Bus socket (required only if `SHUTDOWN_ON_BATTERY_CRITICAL=true`) |\n| `/etc/nut/*.user` | Custom NUT config overrides (e.g. `ups.conf.user`) — bypasses env-var generation |\n\n## Healthcheck\n\nThe built-in healthcheck runs `upsc $UPS_NAME@127.0.0.1` to verify the NUT driver is communicating with the UPS hardware. It becomes unhealthy when the UPS device is disconnected, the driver failed to start, or upsd is not responding, and recovers once the device is reconnected and the driver re-establishes communication.\n\n## Code quality\n\n| Metric | Value |\n|--------|-------|\n| Language | POSIX shell (Alpine) |\n| Entrypoint | 454 lines |\n| Static Analysis | [ShellCheck](https://www.shellcheck.net/) (enforced in CI) |\n| Validation Tests | 242 |\n| Input Validation | Newline injection, numeric, bracket injection, quote injection |\n\nThe entrypoint generates NUT config files from environment variables\nwith security-focused input validation: all values are checked for\nembedded newlines (prevents config injection), bracket characters\n(prevents INI section injection), double-quote characters (prevents\nNUT config quoting breakout), and numeric parameters are validated\nas positive integers. The validation logic is tested via a shared\nreference library with 242 tests. ShellCheck enforced in CI.\n\nNot tested via unit tests: the config file generation and NUT daemon\nstartup — validated on first deploy via the NUT protocol healthcheck\n(queries the UPS directly).\n\n## Security\n\n**No dependency CVEs.** NUT, libmodbus, and net-snmp are compiled\nfrom patched upstream sources via native cross-compilation,\neliminating all CVEs present in Alpine's older packages.\n\n| Tool | Result |\n|------|--------|\n| [shellcheck](https://www.shellcheck.net/) | Clean |\n| [hadolint](https://github.com/hadolint/hadolint) | DL3018 (unpinned apk, accepted) |\n| [gitleaks](https://github.com/gitleaks/gitleaks) | No secrets detected |\n| [trivy](https://trivy.dev/) | 0 dependency CVEs (Alpine base only) |\n| [grype](https://github.com/anchore/grype) | 0 dependency CVEs (Alpine base only) |\n| [semgrep](https://semgrep.dev/) | 1 info (missing USER, expected) |\n\nAll three source versions are tracked by Renovate. The\nmulti-stage build uses [xx](https://github.com/tonistiigi/xx)\nfor native cross-compilation (no QEMU). The entrypoint validates\nall env vars before generating NUT config: newline injection\nprevention, numeric validation, bracket injection checks, and\ndouble-quote injection prevention for config file quoting.\nRuns as root (required for NUT config ownership and USB device\naccess). Host shutdown via D-Bus is gated behind an explicit\nopt-in env var.\n\n**Details for advanced users:** NUT is built with\n`--disable-shared --enable-static` so all binaries are\nself-contained. Config files are 640 root:nut. Admin password\nauto-generated from `/dev/urandom` if not set. All NUT drivers\nare included (USB HID, Modbus, SNMP).\n\n## Dependencies\n\nAll dependencies are updated automatically via [Renovate](https://github.com/renovatebot/renovate) and pinned by digest or version for reproducibility.\n\n| Dependency | Version | Source |\n|------------|---------|--------|\n| tonistiigi/xx | `1.9.0` | [Docker Hub](https://hub.docker.com/_/xx) |\n| alpine | `3.23.4` | [Alpine](https://hub.docker.com/_/alpine) |\n| libmodbus | `v3.1.12` | [GitHub](https://github.com/stephane/libmodbus) |\n| netsnmp | `v5.9.5.2` | [GitHub](https://github.com/net-snmp/net-snmp) |\n| nut | `v2.8.5` | [GitHub](https://github.com/networkupstools/nut) |\n\n## Credits\n\nThis project packages [Network UPS Tools (NUT)](https://github.com/networkupstools/nut) into a container image. All credit for the core functionality goes to the upstream maintainers.\n- [libmodbus](https://github.com/stephane/libmodbus) by\n  [@stephane](https://github.com/stephane) — the Modbus protocol\n  library used by NUT's `apc_modbus` driver\n- [Net-SNMP](https://github.com/net-snmp/net-snmp) — the SNMP\n  library used by NUT's `snmp-ups` driver\n- [xx](https://github.com/tonistiigi/xx) — Dockerfile\n  cross-compilation helper for native multi-platform builds\n\n## Contributing\n\nIssues and pull requests are welcome. Please open an issue first for\nlarger changes so the approach can be discussed before implementation.\n\n## Disclaimer\n\nThese 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.\n\nThis project was built with AI-assisted tooling using [Claude Opus](https://www.anthropic.com/claude) and [Kiro](https://kiro.dev). The human maintainer defines architecture, supervises implementation, and makes all final decisions.\n\n## License\n\nThis project is licensed under the [GNU General Public License v3.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcplieger%2Fdocker-nut-upsd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcplieger%2Fdocker-nut-upsd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcplieger%2Fdocker-nut-upsd/lists"}