A complete, containerized solution for managing graceful shutdowns for UPS-protected servers that lack a direct data monitoring port. It combines a virtual NUT (Network UPS Tools) server, a power monitoring script, a central API hub for client configuration, and a modern web interface for easy management.
Many powerful, cost-effective UPS systems (like sine wave inverters with large external batteries) can power a server rack for hours but offer no way to signal a power failure to the connected devices. This project solves that problem by using a "canary in a coal mine" approach: it monitors several always-on devices (sentinel hosts) that are connected to standard, non-UPS grid power. If all of them go offline simultaneously, the application declares a power failure.
This container acts as the "brain" for a virtual NUT server, allowing standard NUT clients to perform a graceful shutdown. It also provides a central API to manage shutdown configurations for all clients and a modern web interface for easy administration.
This project is the perfect companion to the UPS_monitor client script, which is a lightweight client designed to react to the status changes generated by this Power Manager.
The logic is simple but effective:

- Monitor: A cron job inside the container runs a script (
power_manager.py) which pings a list of user-defined sentinel hosts every 15 seconds (4 checks per minute) for rapid power failure detection. - Decide:
- If at least one sentinel host is online, the script assumes grid power is OK and sets the virtual NUT server's status to
OL(Online). - If all sentinel hosts are offline, the script assumes a power failure and sets the status to
OB LB(On Battery, Low Battery).
- If at least one sentinel host is online, the script assumes grid power is OK and sets the virtual NUT server's status to
- Act & Report:
- NUT clients (
UPS_monitorscripts) periodically check the server's status. When they detectOB LB, they initiate a graceful shutdown countdown. - Clients are responsible for actively reporting their status (e.g.,
online,shutdown_pendingwith remaining time) back to the server's API on every check. - The Web GUI displays a live feed of these reported statuses. It can now show detailed states like
Online,Shutting down...,WoL sent, orStatus Staleif a client stops reporting. - When power is restored, the
power_manager.pyscript waits a configurable delay and sends Wake-on-LAN (WoL) packets to offline clients. It also sets their status toWoL sentin the dashboard, providing clear feedback on the recovery process.
- NUT clients (
- Hardware-Independent: Works with any UPS because it doesn't require a direct data connection.
- Easy Deployment: Fully containerized and managed with a single
docker-compose.ymlfile. - Modern Web GUI: Intuitive web interface for configuration and monitoring, optimized for both desktop and mobile devices.
- Centralized Management API: A lightweight REST API serves client configurations (
/config), live UPS status (/upsc), and receives client status updates (/status), centralizing all interactions. - Live Client Shutdown Monitoring: The dashboard displays a real-time countdown for each client that is preparing for shutdown, providing a clear overview of the system's state during a power outage.
- Power Outage Simulation: Manually trigger a simulated power failure from the web interface to test client shutdown procedures without physically disconnecting power.
- Scheduled Power Outage Simulation: Configure automatic start/stop times for the simulation mode for regular, hands-free testing.
- Standard-Compliant: Controls a standard NUT server, making it compatible with any NUT client (Linux, Windows, Synology DSM, etc.).
- Automated Recovery: Includes a delayed Wake-on-LAN function to automatically restart servers after stable power has returned.
- Unified Configuration: Single configuration file (
power_manager.conf) manages all aspects of the system. - Robust Logging: Includes built-in log rotation and optional, configurable forwarding to a central syslog server like Graylog.
- Email Notifications: Receive real-time alerts for critical events like power outages, power restoration, client status changes, and application errors.
.
βββ app/ # Main application scripts
β βββ power_manager.py # Core monitoring script
β βββ api.py # REST API server
β βββ web_gui.py # Web interface application
β βββ mail_send.py # mail sending script
β βββ templates/ # HTML templates for web GUI
βββ config/ # All user-editable configuration files
β βββ power_manager.conf # Main configuration file
βββ cron/ # Cron job definition
βββ logrotate/ # Log rotation configuration
βββ rsyslog/ # (Optional) Syslog forwarding configuration
βββ .gitignore # Prevents local configs from being committed
βββ Dockerfile # The blueprint for the container image
βββ docker-compose.yml # The easy-run file for Docker Compose
βββ entrypoint.sh # The container's startup script
- A host machine that is connected to the UPS to run the container.
- Docker and Docker Compose installed on the host machine. You may find my Wiki instruction helpful: How to Install Docker Engine on Debian.
- A few reliable, always-on devices with static IPs on your network that are NOT connected to the UPS to act as sentinels.
curl,gitpackages installed on the host.
A recent Docker bug can cause DNS resolution to fail inside containers, potentially affecting features like email notifications. To mitigate this, a dns section has been added to docker-compose.yml.example with fallback public DNS servers.
If you wish to use custom DNS servers, you can configure them in your .env file:
DNS1=192.168.131.152
DNS2=192.168.131.153
Without this configuration, features relying on external network resolution (e.g., sending emails) might cease to function.
-
Clone the Repository:
git clone [https://github.com/MarekWo/UPS_Server_Docker.git](https://github.com/MarekWo/UPS_Server_Docker.git) /opt/ups-server-docker cd /opt/ups-server-docker -
Prepare Docker Compose File: Copy the example Docker Compose file. This ensures your customized file won't be overwritten by future
git pullupdates.cp docker-compose.yml.example docker-compose.yml
You can now edit
docker-compose.ymlif you need to make advanced changes, but for most users, the default is fine. -
Configure the Server: All configuration is now managed through a single file:
./config/power_manager.conf.- First, copy the example configuration file:
cp config/power_manager.conf.example config/power_manager.conf
- Then, edit
config/power_manager.confwith your specific values:SENTINEL_HOSTS: A space-separated list of IPs for your sentinel devices.WOL_DELAY_MINUTES: The time in minutes to wait after power is restored before sending WoL packets.UPS_STATE_FILE: The path to the state file used by thedummy-upsdriver. This must match theportsetting in NUT configuration.API_TOKEN: (Required) The secret token used to authenticate client requests. This value must match the token used by yourUPS_monitorclients.DEFAULT_BROADCAST_IP: The default broadcast address for Wake-on-LAN packets.SMTP Settings:SMTP_SERVERSMTP_PORTSMTP_USE_TLSSMTP_USERNAMESMTP_PASSWORDSMTP_SENDER_NAMESMTP_SENDER_EMAILSMTP_RECIPIENTS
Notification Settings:NOTIFY_POWER_FAILNOTIFY_POWER_RESTOREDNOTIFY_CLIENT_SHUTDOWNNOTIFY_CLIENT_STALENOTIFY_APP_ERRORNOTIFY_SIMULATION_MODE
[WAKE_HOST_X]: Sections defining each server to wake up. Each section requires:NAME: Descriptive name for the hostIP: IP address of the hostMAC: MAC address for Wake-on-LANBROADCAST_IP: (optional) Specific broadcast IP for this hostSHUTDOWN_DELAY_MINUTES: (optional) Makes this host a UPS client with specified shutdown delay. Use0for immediate shutdownAUTO_WOL: (optional): When set to "false" the WoL packet will not be sent to this host automatically
- First, copy the example configuration file:
-
Environment Configuration: This file (
.env) is used to pass crucial settings into the container, such as the host's IP address and your local timezone.- First, copy the example environment file if you haven't already:
cp .env.example .env
- Then, edit the
.envfile and set the following variables:TZ: Your local timezone name (e.g.,Europe/Warsaw).UPS_SERVER_HOST_IP: (Required) The IP address of the Docker host machine. This is the IP that your NUT clients will use to connect to the server (e.g.,192.168.1.10).
- First, copy the example environment file if you haven't already:
-
(Optional) Configure Syslog Forwarding: This allows you to send all internal logs to a central server like Graylog.
- First, copy the example configuration file:
cp rsyslog/custom.conf.example /etc/rsyslog.d/custom.conf
- Then, edit
rsyslog/custom.confand replace the placeholder IP address and port with your syslog server details.
- First, copy the example configuration file:
Here's an example of the unified power_manager.conf file:
# === CONFIGURATION FILE FOR power_manager.py ===
# Sentinel hosts - devices on grid power (not UPS) for monitoring
SENTINEL_HOSTS="192.168.1.11 192.168.1.12 192.168.1.13 192.168.1.14"
# Time to wait after power restoration before sending WoL packets
WOL_DELAY_MINUTES=5
# Secret token for API authentication (must match client configuration)
API_TOKEN="your_super_secret_api_token"
# Enable Power Outage Simulation mode.
# When set to "true", this will force the UPS status to "OB LB" regardless
# of the sentinel hosts' status. Useful for testing shutdown procedures.
# Valid values: "true" or "false".
POWER_SIMULATION_MODE="false"
# Path to the dummy-ups driver's state file
UPS_STATE_FILE=/var/run/nut/virtual.device
# Default broadcast address for WoL packets
DEFAULT_BROADCAST_IP=192.168.1.255
# === SMTP NOTIFICATIONS ===
SMTP_SERVER="smtp.example.com"
SMTP_PORT="587"
SMTP_USE_TLS="auto"
SMTP_USER="user@example.com"
SMTP_PASSWORD="your_password"
SMTP_SENDER_NAME="UPS Server"
SMTP_SENDER_EMAIL="ups@example.com"
SMTP_RECIPIENTS="admin@example.com"
# === NOTIFICATION SETTINGS ===
# Enable or disable notifications for specific events. Valid values: "true" or "false".
NOTIFY_POWER_FAIL="true"
NOTIFY_POWER_RESTORED="true"
NOTIFY_CLIENT_SHUTDOWN="false"
NOTIFY_CLIENT_STALE="true"
NOTIFY_APP_ERROR="true"
NOTIFY_SIMULATION_MODE="true"
# === WAKE-ON-LAN HOST DEFINITIONS ===
# UPS Client with shutdown delay
[WAKE_HOST_1]
NAME=Synology NAS
IP=192.168.1.12
MAC=00:11:32:f8:af:9f
BROADCAST_IP=192.168.1.255
SHUTDOWN_DELAY_MINUTES=10
# Another UPS Client
[WAKE_HOST_2]
NAME=Proxmox Server
IP=192.168.1.13
MAC=00:11:32:2c:31:42
SHUTDOWN_DELAY_MINUTES=15
# UPS Client with immediate shutdown (delay=0)
[WAKE_HOST_3]
NAME=Low-Battery UPS Host
IP=192.168.1.14
MAC=00:11:32:bb:cc:dd
SHUTDOWN_DELAY_MINUTES=0
# WoL-only host (no UPS client functionality)
[WAKE_HOST_4]
NAME=File Server
IP=192.168.1.15
MAC=00:11:32:aa:bb:cc
AUTO_WOL="false"
# === POWER OUTAGE SIMULATION SCHEDULES ===
# [SCHEDULE_1]
# NAME="Weekly Test Shutdown"
# TYPE="recurring"
# DAY_OF_WEEK="friday"
# TIME="23:00"
# ACTION="start"
# ENABLED="true"The SMTP_USE_TLS option provides explicit control over STARTTLS usage:
auto(default): Uses STARTTLS on all ports except 26 (legacy behavior)true: Always attempts STARTTLS (except on port 465 which uses SSL)false: Never uses STARTTLS (for servers that don't support it)
The Wake-on-LAN (WoL) feature requires the container to send special "magic packets" to your network's broadcast address. Docker's default, sandboxed network mode prevents containers from sending broadcast packets to the physical LAN, which means the WoL feature will not work out of the box.
To enable WoL, you must allow the container to share the host's network stack.
The simplest way to enable WoL is to add the network_mode: host directive to your docker-compose.yml file. This gives the container direct access to the host's network interfaces.
Your docker-compose.yml should be modified to include this line:
services:
ups-server:
build: .
container_name: ups-server
restart: unless-stopped
network_mode: host # This line enables WoL functionality
# NOTE: The 'ports' section is ignored in host mode and can be removed.
# ports:
# - "3493:3493"
# - "5000:5000"
# - "80:80"
# ... rest of your configurationSecurity Note: Using network_mode: host removes network isolation between the container and the host. The container gains access to all of the host's network interfaces and can bind to any port. While this application is built to be trustworthy, you should always be aware of the security implications of this setting.
If you do not need the Wake-on-LAN feature or are not comfortable with using host network mode, simply do not add the network_mode: host line.
In this case, the NUT server and API will function correctly for monitoring and shutting down clients, but the ability to automatically wake them up will be disabled.
Once configured, starting the server is a single command from the project's root directory:
docker compose up --build -d--build: Only needed the first time or after changing theDockerfileor application scripts.-d: Runs the container in the background (detached mode).
Managing the Container:
- View Logs:
docker compose logs -f
- Stop the Container:
docker compose down
- Restart the Container:
docker compose restart
You can also monitor the power_manager.log file in the ./logs directory, which is created automatically on your host.
The UPS Server includes a modern, responsive web interface for easy configuration and monitoring.
After starting the container, the web interface will be available at:
http://<UPS_SERVER_IP>
For example, if your UPS server host has IP 192.168.1.10:
http://192.168.1.10
- Dashboard: Real-time monitoring of system status, sentinel hosts, and UPS clients. Shows a live countdown for clients that are shutting down.
- Configuration Management: Easy-to-use forms for managing all system settings.
- Live Status Updates: Automatic refresh of host statuses every 5 seconds.
- Mobile Optimized: Responsive design that works on all devices.
- One-Click Wake-on-LAN: Send WoL packets directly from the interface.
For detailed Web GUI documentation, see WEB_GUI_README.md.
The server provides a REST API for client configuration and status monitoring. All endpoints require an Authorization header with a bearer token. The API token is now configured in config/power_manager.conf.
Example Request:
curl -H "Authorization: Bearer <your_secret_token>" http://<server_ip>:5000/upscThis endpoint provides client-specific shutdown configuration. The client's IP address is used to look up its settings in the WAKE_HOST_X sections of power_manager.conf.
- Query Parameter:
ip=<client_ip>(optional, falls back to request source IP). - Returns: A JSON object with the client's configuration, including the dynamically generated
UPS_NAMEandSHUTDOWN_DELAY_MINUTES.
Example Response (/config?ip=192.168.1.12):
{
"SHUTDOWN_DELAY_MINUTES": "10",
"UPS_NAME": "ups@192.168.1.10"
}This endpoint allows UPS clients to report their current status back to the server. This is used to display the shutdown countdown in the Web GUI.
- Body: A JSON object containing the client's status.
- Example Payload:
{ "ip": "192.168.1.12", "status": "shutdown_pending", "remaining_seconds": 245, "shutdown_delay": 5 }
This endpoint provides live status information from the NUT server, equivalent to running the upsc command locally, but with clean, nested JSON output.
- Returns: A nested JSON object containing all available UPS variables, plus additional simulation status information. This is ideal for monitoring dashboards or advanced client-side logic.
- Simulation Detection: The response includes a
simulationfield in theupssection that indicates whether the current power outage status is real (false) or simulated (true).
Example Response:
{
"device": {
"mfr": "Dummy Manufacturer",
"model": "Dummy UPS",
"type": "ups"
},
"driver": {
"name": "dummy-ups",
"parameter": {
"mode": "dummy",
"pollinterval": 2,
"port": "/var/run/nut/virtual.device"
},
"version": "2.8.0",
"version.internal": 0.15
},
"ups": {
"mfr": "Dummy Manufacturer",
"model": "Dummy UPS",
"status": "OL",
"simulation": false
}
}Simulation Status Field:
The simulation field in the ups section indicates the current power outage simulation status:
false: Normal operation - UPS status reflects real power conditionstrue: Simulation mode active - UPS status is artificially set for testing purposes
This field is read from the POWER_SIMULATION_MODE parameter in power_manager.conf and allows UPS clients to distinguish between real power outages and simulated ones for testing.
The container provides the following services:
- Port 80: Web GUI interface
- Port 5000: REST API for client configuration
- Port 3493: NUT server for UPS clients
To update the application to the latest version from GitHub, follow these steps. This method is robust and will discard any accidental local changes, ensuring a clean update.
-
Navigate to the application directory
cd /opt/ups-server-dockerNote:
sudois likely required for the following commands if you cloned the repository into a system directory like/opt. -
Fetch the latest version from the repository This command downloads the latest updates from GitHub.
sudo git fetch origin
-
Reset your local files to match the latest version This command will discard any local changes (like modified permissions or accidental edits) and force your local copy to match the official version.
sudo git reset --hard origin/main
-
Rebuild and restart the container This applies the updates and restarts the application.
sudo docker compose up --build -d
Docker Compose will intelligently rebuild only what's necessary and restart the container. Your configuration files in the
./configdirectory will be preserved.
If you're upgrading from a version that used separate upshub.conf configuration:
-
Backup your current configuration:
cp config/power_manager.conf config/power_manager.conf.backup cp config/upshub.conf config/upshub.conf.backup
-
Migrate UPS client settings: Add
SHUTDOWN_DELAY_MINUTESparameter to appropriate[WAKE_HOST_X]sections inpower_manager.confbased on your oldupshub.confsettings. -
Remove old configuration:
rm config/upshub.conf
-
Update and restart:
docker compose up --build -d
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this project better, please feel free to share it. The preferred way to do so is by starting a conversation in the Discussions section of the repository.
If you would like to contribute with your code, please fork the repo and create a pull request.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Thank you for helping make this tool better!
Have you found a bug or have an idea for a new feature? I would love to hear from you!
To ensure that ideas are well-discussed and bugs are properly triaged, this project uses GitHub Discussions as the first step for all new reports.
How to submit an issue or idea:
- Go to the Discussions tab and open a new discussion in the "Ideas" category.
- Provide a clear title and a detailed description of the issue or your suggestion. If you're reporting a bug, please include:
- Steps to reproduce the behavior.
- What you expected to happen.
- What actually happened (screenshots are welcome!).
- Your environment details (e.g., Docker version, host OS).
- Engage in the discussion. I will review your post and may ask follow-up questions.
- From Discussion to Issue. If the report is confirmed as a bug or the feature is considered for implementation, I will create an official
Issuedirectly from your discussion thread to track its progress.
This process helps keep the official issue tracker clean and focused on actionable items. Thank you for your understanding and cooperation!
This project is licensed under the MIT License - see the LICENSE file for details.
