Outage Tracker is a lightweight, self-hosted Docker application designed to monitor your neighborhood's power grid, your local home rack's battery health, and your home internet connection simultaneously.
While standard UPS notification scripts run locally and fail if your home internet goes down, Outage Tracker is designed to be hosted externally (like on a Cloud VPS). It queries your utility company's API to track grid failures in your area, while tunneling into your Network UPS Tools (NUT) server via a built-in mesh VPN to monitor your local battery runtime.
Main Page:
History Logs:
If you already have a UPS, why do you need to poll the power company and your internet connection from a remote server?
- The "Dead Internet" Problem: If your neighborhood loses power, the coax/fiber node down the street might lose power too. Even if your servers and router are on a UPS, your home internet drops. A local NUT server can't send you an email/push notification without internet. Because Outage Tracker runs on a remote VPS, it will see the utility company report the outage and alert you, even if your house is completely offline.
- The "Neighborhood" View: The Utility API tells you what is happening in your Zip Code. You can get alerted about a major outage hitting your neighborhood while you are at work, before you even get home.
- The "Local Rack" View: Meanwhile, the NUT integration tells you exactly what is happening to your physical hardware. If the power drops, Outage Tracker monitors the exact battery percentage and runtime of your UPS array, sending critical alerts when your servers are about to die.
- The "ISP Outage" View (Watchdog): Sometimes the power is perfectly fine, but your ISP goes down. The built-in Network Watchdog continuously pings your home network's IP or Tailscale node from the outside. If it drops, your VPS immediately alerts you that your home internet is offline, completely independent of the power grid!
- Grid Monitoring (KUBRA API): Natively supports tracking any major utility company that uses the KUBRA Storm Center platform (Georgia Power, Duke Energy, Alabama Power, FirstEnergy, Entergy) as well as Pacific Power.
- Auto-Discovery & Auto-Heal: Simply provide the main outage map URL, and the app will automatically hunt down the hidden JSON APIs in the background. If the utility company rotates their URLs (which KUBRA does periodically), the app detects the
404failure and instantly auto-heals its own configuration without you lifting a finger! - Multi-UPS Array Support: Connects to your local NUT server. Use the
autosetting to automatically discover and independently track every UPS in your server rack. - Dual-WAN Watchdog: Continuously pings up to two IP addresses (like your home Public IP or a Tailscale node) to monitor your home internet connection for downtime, with independent alerting.
- SNMP Monitoring: Poll up to two SNMP devices using SNMPv2c or SNMPv3 with configurable host, port, and OID values. This also works for cloud SNMP targets (including Meraki-reachable monitoring endpoints). You will be alerted when the tracked uptime OID decreases, mathematically proving a reboot or power event.
- MQTT / Home Assistant Export: Publish the exact live status from the main dashboard into Home Assistant over MQTT, including grid outage state, UPS runtime, watchdog health, and SNMP device status with automatic Home Assistant discovery. The backend also republishes a retained heartbeat automatically every 5 minutes when idle and every 30 seconds during active events so Home Assistant sensors do not go stale.
- Dynamic Full-Width Layout: All pages dynamically stretch up to 1500px to offer widescreen views based on your preference. Supports both 1x4 Ultra-Wide rows or compact 2x2 NVR-style grids that automatically adjust depending on which modules you use.
- Smart UI Refresh: To save network bandwidth, the dashboard quietly refreshes every 5 minutes when everything is normal. The exact second a grid outage or UPS event is detected, it shifts into high gear and refreshes every 30 seconds for real-time monitoring.
- Built-in Tailscale VPN: No need to install VPN software on your Docker host. Outage Tracker runs its own internal Tailscale daemon to securely bridge your cloud VPS to your home network. The Settings page shows the installed Tailscale version and lets you apply available updates in-place, without waiting for a new container image.
- Zero-Knowledge Security: Sensitive API tokens, VPN auth keys, and home coordinates are configured as "Write-Only" in the UI. Once saved, they are encrypted and hidden from the frontend to protect your data.
- Rich Map Notifications: Optionally integrate a free Mapbox API key to instantly generate and attach a street-level map of the outage area directly to your phone's lock screen.
- Event History Logs: Persistently tracks the duration, severity, and timestamps of every local grid outage, UPS battery event, hardware reboots, and network downtime so you can review your infrastructure stability over time. The view limits to the 2 most recent events to keep things tidy, with an option to load the full history logs.
- Built-in Time Zone: The app manages internal clocks entirely from a UI dropdown meaning there is no longer a need to mount TZ data into Docker compose!
You only need the compose.yaml file to deploy the published image. Ensure your deployment directory looks like this:
outage-tracker/
βββ compose.yaml
βββ auth_key/ <-- Auto-generated folder containing your secret decryption key
βββ data/ <-- Auto-generated folder containing configuration and history
Create a new directory on your Docker host and navigate into it:
mkdir outage-tracker
cd outage-trackerDeploy via Docker Compose. The application uses persistent volumes to save your settings, Tailscale identity, encryption keys, and history logs so they survive container updates.
services:
outage-tracker:
image: nighthawkatl/outage-tracker:latest
container_name: outage-tracker
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./data:/app/data
- ./auth_key:/app/auth_key # Persists the encryption key securely outside the data folder
- /dev/net/tun:/dev/net/tun # Required for Tailscale networking
cap_add:
- NET_ADMIN # Required for Tailscale networking
- NET_RAWStart the container:
docker compose up -dOnce running, access the dashboard at http://<YOUR-DOCKER-IP>:8080.
On your first boot, you will be met with a secure login screen.
Default Username: admin
Default Password: admin
(Note: Please log in and immediately change your password in the Settings page!)
π Forgot your password? If you get locked out of your dashboard, SSH into your docker host and run
docker exec -it outage-tracker python reset_auth.py. Your password will instantly reset toadminwithout deleting any of your saved utility or VPN settings!
The app will initially load as a "Blank Slate". Click the βοΈ Settings button in the top right of the dashboard to configure your tracker. The Save Configuration bar floats at the bottom of the screen with a subtle backdrop blur as you scroll through settings, and snaps to its natural footer position when you reach the bottom.
If you are running this on a Cloud VPS, do not port-forward your home router to expose your NUT server to the internet.
- Generate an Auth Key from your Tailscale Admin Console.
- Paste it into the Web UI. The container will instantly authenticate and join your Tailnet, allowing you to securely ping your home server's
100.x.x.xIP address. - Accept Subnet Routes is off by default. Only enable it if you specifically need to reach devices behind another tailnet subnet router β enabling it means this container will accept and route through any subnet another tailnet device advertises, which can conflict with its own local network and lock you out of the app itself (see Tailscale Route Hijack below). If that happens, run
docker exec -it <container> tailscale set --accept-routes=falsefrom the Docker host to recover. - The Settings page compares your installed Tailscale version against Tailscale's actual latest upstream release (not just Alpine's package mirror), checking on the schedule you choose (Daily, Weekly, or None/Manual Only), or on demand via the Check Now button. If a newer version is available, an Update Tailscale button lets you apply it in place. Note: the update itself still installs whatever version Alpine's
tailscalepackage currently offers, which can lag behind the very latest upstream release β the badge will tell you if that's the case. If an update check or package upgrade encounters an error, an unobtrusiveβ οΈ Errors badge appears in the card header with a hover popover detailing the last 5 errors and timestamps.
To track your local power grid, the app uses an Auto-Discovery engine:
- Enter your Zip Code.
- Paste the URL to your utility's main outage map (e.g.,
https://outagemap.georgiapower.com/) into the Map Website URL field. - Leave the Outage API JSON URL field entirely blank, and hit Save Configuration.
The app will instantly scan the website in the background, bypass any iframes, locate the hidden JSON API endpoints, and save them automatically!
Manual Fallback: If auto-discovery fails, you can easily find the API URL yourself:
- Open your power company's outage map in your desktop browser.
- Press F12 to open Developer Tools and navigate to the Network tab.
- In the Network filter box, type
json. - On the actual Map UI, find the Map Legend / Menu and change the view mode from "Clusters" (circles) to "View by Zip/City" or "Zip Code".
- The exact moment the map shades the zip codes, a new file will appear at the bottom of your Network tab (usually named
thematic_areas.jsonorlistCA.json). - Click that file, copy its Request URL, and paste it into the UI.
If you run a local NUT server, enter its IP and Port.
- Set UPS Names to
autoto automatically fetch every UPS attached to the server, or list them manually (e.g.,nutdev1,nutdev2).
Continuously ping up to two devices to detect ISP or local network failures. You can enter your home's Public IP, a Dynamic DNS hostname, or a secure Tailscale IP (e.g., 100.x.x.x).
- If your target goes offline longer than the configured threshold, a network downtime alert will be sent.
- Configure the secondary failover target for multi-WAN setups.
- When the target port is
443, the watchdog completes a full TLS handshake instead of an abrupt connect-and-close. This means the check looks like a normal HTTPS client to reverse proxies and WAF/bouncer tools (e.g., CrowdSec behind Zoraxy), so you don't need to keep port 80 open on your home firewall just to satisfy the watchdog. - ISP & Outage Intelligence: Automatically resolves your target's public IP to identify the ISP name, Autonomous System Number (ASN), and metro area. When a drop occurs, the watchdog cross-references your local power grid status (detecting if neighborhood node power is down), queries the Cloudflare Radar API for active backbone/BGP routing disruptions, and enriches dashboard and Pushover alerts with direct 1-click status links (e.g., Downdetector). Tailscale/private IP targets can optionally supply a fallback public IP/hostname in Settings.
- Radar & Downdetector Tuning: Enter your free Cloudflare Radar API token in Settings to enable BGP disruption checks, and optionally customize the Downdetector provider slug (e.g.,
point-broadband,comcast-xfinity) if you use a regional or specialized ISP.
To receive rich map images of your neighborhood attached to your Pushover alerts:
- Go to Mapbox.com and create a free account.
- Copy your Default Public Token (
pk.eyJ1...) from Mapbox. - Enter the Token, plus your exact home Latitude and Longitude in the Web UI.
Create a free account at Pushover.net and create an "Application" to get your API Token. You will need two specific keys for the Web UI:
- User Key: Found on your main Pushover dashboard immediately after logging in.
- API Token: Found by clicking your specific Application under "Your Applications".
Use the "Test Pushover Alert" button on the main dashboard to verify your keys are correct and preview your Mapbox generation!
If you want the live dashboard state in Home Assistant, install the Mosquitto broker addon in Home Assistant, add the built-in MQTT integration, and create a dedicated MQTT user account.
- In Home Assistant, keep the Tailscale addon connected to your tailnet so the broker is reachable over its
100.x.x.xaddress or MagicDNS hostname. - In Outage Tracker, open Settings and fill in the MQTT Host / IP, Port, Username, and Password fields.
- Leave the default Topic Prefix (
outage_tracker) and Discovery Prefix (homeassistant) unless you already use a different namespace. - Save the configuration. Outage Tracker will immediately begin publishing retained MQTT status payloads and Home Assistant discovery entities for the grid, UPS, watchdog, and SNMP panels on the dashboard.
- The card displays a live Last Published timestamp (updated every 5 minutes when idle, and every 30 seconds during active events) alongside a π‘ Republish MQTT button to manually re-push all retained states and discovery payloads on demand. If publish or connection issues occur, a hover-over
β οΈ Errors badge appears in the header with the last 5 error details and timestamps.
Tailscale Note: You do not need to enable Funnel, Serve, Exit Node, or open public ports just for MQTT if Mosquitto is running on the Home Assistant host itself. Simply keep Home Assistant joined to the same tailnet as Outage Tracker.
Depending on your configuration and the events that occur, Outage Tracker will push rich notifications directly to your device. Here is exactly what those alerts will look like (using Georgia Power, Zip Code 12345, and a UPS named nutdev1 as examples):
Active Outage Alert (Priority 1 - High) Triggered when the utility company API reports an outage in your zip code that outlasts your configured threshold.
Title: π¨ Georgia Power Outage Alert
Message:
Power out in 12345 for >10 mins.
Affected: 1245
Est. Restoration: Today at 5:00 PM
Attachment: πΊοΈ (A dark-themed Mapbox street map of your exact home coordinates)
Grid Power Restored (Priority 0 - Normal) Triggered when the utility company API updates to show 0 customers affected in your zip code.
Title: β Georgia Power Power Restored
Message:
Restored in 12345!
Outage lasted 112 mins.
Critical Battery Alert (Priority 1 - High) Triggered immediately when your local UPS switches to "On Battery" (OB) and its estimated runtime drops below your configured minimum (e.g., 10 minutes).
Title:
β οΈ CRITICAL: nutdev1 Low!
Message:
UPS 'nutdev1' on battery, 9 mins left.
UPS Power Restored (Priority 0 - Normal) Triggered when your UPS switches back to "On Line" (OL) grid power after an event.
Title: π UPS nutdev1 Restored
Message:
Back on grid power.
Network Offline (Priority 1 - High) Triggered when the watchdog fails to connect to the target for your configured threshold limit. Automatically enriched with detected ISP/ASN details, power grid correlation, Cloudflare Radar status, and direct outage links.
Title: π
β οΈ Network Offline
Message:
Primary WAN (Point Broadband) connection to 204.116.x.x:443 failed for >5 mins.
ASN: AS400548 Β· Newnan, Georgia
β οΈ Power grid outage active in 30265 (Node power likely down).
Cloudflare Radar: Normal (No active BGP/ASN disruption)
Check status: https://downdetector.com/status/point-broadband/
Network Restored (Priority 0 - Normal) Triggered when the network target becomes reachable again.
Title: β Network Restored
Message:
Primary WAN connection to 204.116.x.x:443 restored.
Downtime: 45 mins.
Manual UI Test (Priority 0 - Normal) Triggered when you click the "Test Pushover Alert" button on your dashboard.
Title: π Pushover Test
Message:
Configuration working perfectly. Here is your map!
Attachment: πΊοΈ (Your generated Mapbox image)
A Note on Priorities: Active Grid Outages, Critical UPS alerts, and Network Offline alerts are sent as Priority 1. In Pushover, this means the notification will play a sound and vibrate your phone even if your phone is set to "Quiet Hours". The "Restored" and "Test" messages are sent as Priority 0, meaning they will deliver normally and won't wake you up in the middle of the night just to tell you the power came back on!
If you are operating a multi-VLAN homelab or a "Zero Trust" environment, you may encounter [Errno 111] Connection refused when Outage Tracker attempts to reach your NUT server. Here is how to resolve cross-VLAN and VPN routing issues:
By default, NUT only listens to localhost. You must tell it to listen to your Tailscale and VLAN interfaces.
Edit /etc/nut/upsd.conf on your NUT server:
# Comment out other LISTEN directives and add:
LISTEN 0.0.0.0 3493
Restart the service: sudo systemctl restart nut-server
If your NUT Server runs a host-level firewall like UFW, it will block incoming connections from the VPN and other local subnets. Run the following on your NUT server to allow traffic:
# Allow Tailscale VPN traffic
sudo ufw allow in on tailscale0 to any port 3493
# Allow internal cross-VLAN traffic (e.g., from Home Assistant)
sudo ufw allow from 10.0.0.0/8 to any port 3493If local devices suddenly lose access to your NUT server (if it is on a different VLAN) after installing Tailscale, you are likely experiencing Asymmetric Routing. The NUT server receives the local packet, but attempts to send the reply back through the Tailscale tunnel instead of your physical router.
This same issue can affect Outage Tracker's own container if Accept Subnet Routes is enabled in Settings and another tailnet device advertises a subnet that overlaps with the container's network β traffic back to your browser can get pulled into the tunnel instead of reaching you, making the app appear to hang or become unreachable. Accept Subnet Routes is off by default specifically to avoid this; only turn it on if you understand the risk.
To fix this, disable route acceptance so it ignores Tailscale subnets and respects your physical router's routing table:
sudo tailscale up --accept-routes=falseIf this happens to the Outage Tracker container itself and you're locked out of the Web UI, run this from the Docker host instead:
docker exec -it <container-name> tailscale set --accept-routes=false(Alternatively, create a Layer 3 pinhole rule in your primary router/firewall to pass traffic directly between the VLANs, keeping local traffic entirely off the VPN).
- Backend: Python 3.11 (Raw TCP sockets for NUT, Requests for API polling)
- Frontend: Flask, HTML5, Bootstrap 5 (Dark Mode)
- Container: Alpine Linux (Minimal footprint)
MIT License. Feel free to fork, modify, and expand!
This is a current work in progress. It was coded with the help of AI to get the base project running with my ideas.