2026-09-21 07:14:56 +00:00
2026-09-17 02:38:26 +00:00
2026-09-17 02:38:26 +00:00
2026-09-17 03:10:01 +00:00
2026-09-21 07:14:56 +00:00

nut-relay

A lightweight NUT (Network UPS Tools) relay/repeater that lets you bypass the client connection limit enforced by many consumer/SMB UPS units with a built-in NUT server (e.g. UniFi UPS, some APC/CyberPower "smart" models).

Why this exists

Some UPS units expose a NUT server directly, but cap the number of simultaneous clients allowed to connect (for example, 3 clients on the UniFi UPS 2U). If you have more machines than that to monitor and gracefully shut down on power loss, you hit that wall fast.

nut-relay solves this by sitting between your UPS and all your machines:

  • It connects to your UPS's NUT server as a single client (using up just one of the few available slots).
  • It runs its own NUT server, which has no artificial client limit.
  • All your machines then connect to the relay instead of the UPS directly.
Your UPS (NUT server, limited clients)
        │  single connection (repeater mode)
        ▼
   nut-relay (this project)
        │  unlimited connections
        ├── client 1
        ├── client 2
        ├── client 3
        └── ... as many as you need

Under the hood, this uses NUT's dummy-ups driver in repeater/netclient mode — despite the name, it relays real, live data from your UPS; it is not a simulated device.

Requirements

  • Docker + Docker Compose on the machine that will run the relay
  • Network access from that machine to your UPS's NUT server (default port 3493)
  • The UPS's NUT device name, host/IP, and a NUT username/password with read access (usually configured in your UPS's own management interface)

Server Setup (the relay)

1. Create a project folder

mkdir nut-relay && cd nut-relay

2. Create docker-compose.yml

services:
  nut-relay:
    image: git.ericampire.app/ericampire/nut-relay:latest
    container_name: nut-relay
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./config:/etc/nut

network_mode: host is recommended so the container shares the host's network stack directly — this matters if your UPS lives on a different network segment/VLAN than Docker's default bridge network can reach. If your setup doesn't need that, you can instead use a ports: mapping:

    ports:
      - "3493:3493"

(don't combine both — ports: is ignored/redundant under network_mode: host)

If you'd rather build the image yourself instead of pulling the published one, replace image: with build: . and make sure the Dockerfile and entrypoint.sh from this repo are in the same folder.

3. Create your config folder

cp -r config.example config

This gives you config/nut.conf, config/ups.conf, config/upsd.conf, and config/upsd.users as a starting point — edit each one for your setup below.

4. Edit config/ups.conf

Point it at your actual UPS. The port value must be in the form <remote_ups_name>@<remote_host>:<remote_port> for repeater mode to work (a plain filename like xxx.dev would instead run in static/dummy mode).

[relayups]
    driver = dummy-ups
    port = <your_ups_nut_name>@<your_ups_ip>:3493
    desc = "NUT relay/repeater"

Not sure of your UPS's NUT device name? Run: docker run --rm --network host <image> upsc -l <your_ups_ip>

5. Edit config/upsd.users

One account for the relay-to-UPS connection, and one unique account per client you plan to connect:

[relay-to-ups]
    password = <your_ups_nut_password>
    upsmon primary

[client-01]
    password = <unique_strong_password>
    upsmon secondary

[client-02]
    password = <another_unique_password>
    upsmon secondary

Generate strong passwords with:

openssl rand -base64 24

6. Start the relay

docker compose up -d
docker compose logs -f

7. Verify

docker exec -it nut-relay upsc relayups@localhost

You should see real live values (battery charge, load, status, etc.) — not Dummy Manufacturer placeholder data. If you see placeholder data, double check the port = line in ups.conf.

Client Setup (each machine to protect)

Install nut-client natively on each machine you want to monitor and shut down gracefully on power loss.

1. Install

apt install nut-client -y      # Debian/Ubuntu

2. /etc/nut/nut.conf

MODE=netclient

3. /etc/nut/upsmon.conf

MONITOR relayups@<relay_ip>:3493 1 client-01 <unique_strong_password> secondary

MINSUPPLIES 1
SHUTDOWNCMD "/sbin/shutdown -h +0"

POLLFREQ 5
POLLFREQALERT 5
DEADTIME 15

NOTIFYFLAG ONLINE   SYSLOG
NOTIFYFLAG ONBATT   SYSLOG+WALL
NOTIFYFLAG LOWBATT  SYSLOG+WALL+EXEC
NOTIFYFLAG FSD      SYSLOG+WALL+EXEC
NOTIFYFLAG COMMBAD  SYSLOG+WALL
NOTIFYFLAG COMMOK   SYSLOG
NOTIFYFLAG SHUTDOWN SYSLOG+WALL

Use the matching account name/password you created for this client in the relay's upsd.users.

4. Enable and start

systemctl enable nut-client nut-monitor
systemctl restart nut-client nut-monitor

5. Verify

systemctl status nut-monitor
upsc relayups@<relay_ip>:3493

A successful connection log line looks like:

UPS: relayups@<relay_ip>:3493 (secondary) (power value 1)

How the shutdown actually triggers

A common misunderstanding: going on battery does not shut anything down.

Power loss   → status = OB (on battery)   → just a notification, nothing shuts down
Low battery  → status = LOWBATT           → relay sends FSD to all clients
                                           → each client runs its SHUTDOWNCMD

primary vs secondary in upsd.users controls who is allowed to trigger the forced shutdown broadcast — it does not control individual timing. All secondary clients receive the same shutdown signal at the same time, based on the low-battery threshold configured on the UPS itself. If you need a staggered/cascaded shutdown order instead, look into NUT's upssched (not covered by this basic relay setup).

Security notes

  • Never commit your real config/ folder — only config.example/ should be version-controlled.
  • Give every client its own unique password; don't reuse one password across machines.
  • The relay-to-UPS account only needs read access; it does not need to be able to trigger shutdowns on the source UPS.
S
Description
No description provided
Readme
38 KiB
Languages
Shell 59.9%
Dockerfile 40.1%