Sign inSign up

noxied/wanwatcher

By noxied

•Updated 4 months ago

WAN IP monitoring with Discord, Telegram, Email alerts. Lightweight & Docker-ready for homelabs!

Image
Networking
Developer tools
Monitoring & observability
1

5.9K

noxied/wanwatcher repository overview

⁠WANwatcher

WANwatcher Banner

Monitors your WAN IPv4/IPv6 addresses and tells you when they change. When an address changes it can notify you, update DNS records, publish the state over MQTT for Home Assistant, and expose a status API with Prometheus metrics. It is aimed at homelabs and small servers on connections where the ISP changes your IP.

GitHub Release Docker Pulls Docker Image Size License


⁠Recent releases

  • 2.5.0: reliability hardening (stuck-loop /healthz, isolated notifier failures, locked status reads).
  • 2.4.1: security fix - escape untrusted geo/release-note strings in notifications.
  • 2.3.0: AWS Route53 DDNS provider (SigV4, no AWS SDK bundled).
  • 2.2.0: secrets from files (<NAME>_FILE), plus Trivy scanning, CycloneDX SBOM and Cosign keyless image signing.
  • 2.1.0: optional JSON logging (LOG_FORMAT=json) and adaptive backoff with jitter.
  • 2.0.0: rebuilt as a Python package with Apprise, multi-source detection, DDNS, status API/metrics, MQTT/Home Assistant, events, and a non-root container.

Coming from 1.x, the one breaking change is that the container runs as uid 1000, so the /data and /logs volumes must be writable by that user. See the upgrade guide⁠.

Full changelog⁠


⁠Features

Notifications

  • Discord webhooks with embeds
  • Telegram bot messages
  • Email over SMTP with HTML and plain-text bodies
  • Apprise, covering 100+ services from one APPRISE_URLS setting

IP detection

  • IPv4 and IPv6, each can be turned off independently
  • Several detection services tried in rotating order so one broken or rate-limited service does not block detection
  • A detected change is confirmed against a second source before you are notified

Dynamic DNS

  • Cloudflare (API token), DuckDNS, AWS Route53, and any dyndns2-compatible provider
  • Failed updates are retried on the next check

Observability

  • HTTP status API: /healthz, /api/status (JSON), and Prometheus /metrics
  • MQTT publishing with Home Assistant auto-discovery
  • Optional geographic data (city, region, country, ISP, timezone) via ipinfo.io

Events

  • Notice on startup, optional heartbeat, and internet outage detection with a recovery notification

Operations

  • Graceful shutdown on SIGTERM, atomic state file writes with a change history
  • Retry with exponential backoff for notifications
  • Runs as a non-root user (uid 1000)
  • Multi-architecture image (AMD64 and ARM64)

⁠Supported architectures

ArchitectureTagsStatus
x86-64 (AMD64)latest, 2.5.0Supported
ARM64 (aarch64)latest, 2.5.0Supported

Docker pulls the correct image for your platform automatically. ARM64 covers Raspberry Pi 4 and newer, Apple Silicon, and AWS Graviton.


⁠Quick start

Create the host directories and make them writable by uid 1000:

mkdir -p data logs
sudo chown -R 1000:1000 data logs
⁠docker run (Discord)
docker run -d \
  --name wanwatcher \
  --restart unless-stopped \
  -e DISCORD_ENABLED="true" \
  -e DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN" \
  -e SERVER_NAME="My Server" \
  -v ./data:/data \
  -v ./logs:/logs \
  noxied/wanwatcher:2.5.0
⁠docker compose
services:
  wanwatcher:
    image: noxied/wanwatcher:2.5.0
    container_name: wanwatcher
    restart: unless-stopped
    environment:
      SERVER_NAME: "My Server"
      CHECK_INTERVAL: "900"
      DISCORD_ENABLED: "true"
      DISCORD_WEBHOOK_URL: "https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"
    volumes:
      - ./data:/data
      - ./logs:/logs
docker compose up -d

⁠Configuration

Everything is set through environment variables. Booleans are the string "true" (anything else is false). Lists are comma separated. At least one notification method must be enabled.

⁠Discord
DISCORD_ENABLED: "true"
DISCORD_WEBHOOK_URL: "https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"
DISCORD_AVATAR_URL: ""   # optional; the webhook's own avatar is used when empty
⁠Telegram
TELEGRAM_ENABLED: "true"
TELEGRAM_BOT_TOKEN: "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
TELEGRAM_CHAT_ID: "123456789"
TELEGRAM_PARSE_MODE: "HTML"
⁠Email (SMTP)
EMAIL_ENABLED: "true"
EMAIL_SMTP_HOST: "smtp.gmail.com"
EMAIL_SMTP_PORT: "587"          # 587 for TLS, 465 for SSL
EMAIL_SMTP_USER: "[email protected]"
EMAIL_SMTP_PASSWORD: "app-password"
EMAIL_FROM: "[email protected]"
EMAIL_TO: "[email protected]"   # comma separated for multiple recipients
EMAIL_USE_TLS: "true"
⁠Apprise

One setting opens 100+ services. See the Apprise URL list⁠.

APPRISE_ENABLED: "true"
APPRISE_URLS: "ntfy://ntfy.sh/my-topic,pover://user@token"
⁠Dynamic DNS
DDNS_ENABLED: "true"
DDNS_PROVIDER: "cloudflare"     # cloudflare | duckdns | dyndns2 | route53

# Cloudflare
CLOUDFLARE_API_TOKEN: "token-with-zone-dns-edit"
CLOUDFLARE_ZONE: "example.com"
CLOUDFLARE_RECORDS: "home.example.com,vpn.example.com"
CLOUDFLARE_PROXIED: "false"

# DuckDNS
DUCKDNS_TOKEN: "your-token"
DUCKDNS_DOMAINS: "mysub"

# dyndns2 (No-IP, Dynu, and compatibles)
DYNDNS2_SERVER: "https://dynupdate.no-ip.com"
DYNDNS2_USERNAME: "user"
DYNDNS2_PASSWORD: "password"
DYNDNS2_HOSTNAMES: "host.example.com"

# AWS Route53 (creds need route53:ChangeResourceRecordSets on the zone)
ROUTE53_ACCESS_KEY_ID: "AKIA..."
ROUTE53_SECRET_ACCESS_KEY: "your-secret"
ROUTE53_HOSTED_ZONE_ID: "Z1234567890ABC"
ROUTE53_RECORDS: "home.example.com"
⁠Status API and MQTT
API_ENABLED: "true"
API_PORT: "8080"                # publish this port to reach the API

MQTT_ENABLED: "true"
MQTT_HOST: "192.168.1.10"
MQTT_PORT: "1883"
MQTT_USERNAME: "wanwatcher"
MQTT_PASSWORD: "secret"
MQTT_HA_DISCOVERY: "true"       # sensors appear in Home Assistant automatically
⁠General and events
CHECK_INTERVAL: "900"           # seconds between checks (minimum 60)
MONITOR_IPV4: "true"
MONITOR_IPV6: "true"
IP_CHANGE_CONFIRMATION: "true"  # confirm a change with a second source
IPINFO_TOKEN: ""                # optional ipinfo.io token for geo data

NOTIFY_ON_STARTUP: "true"
HEARTBEAT_ENABLED: "false"
HEARTBEAT_INTERVAL: "86400"
OUTAGE_DETECTION_ENABLED: "true"
OUTAGE_THRESHOLD: "3"

UPDATE_CHECK_ENABLED: "true"
UPDATE_CHECK_INTERVAL: "86400"

The full reference with every variable and default is in the README⁠.


⁠Volumes and ports

MountPurpose
/dataState file with the current IPs and change history (persistent)
/logsApplication logs

Port 8080 only needs publishing when API_ENABLED=true.


⁠Health check

The image ships a healthcheck. With the status API enabled it queries /healthz; otherwise it confirms the state file is valid JSON and was refreshed within the expected interval, so a stuck loop is reported as unhealthy.

docker inspect --format='{{json .State.Health}}' wanwatcher

⁠Monitoring and logs

View logs:

docker logs -f wanwatcher

Read the current state:

docker exec wanwatcher cat /data/ipinfo.db

With API_ENABLED=true, query the status and metrics:

curl http://localhost:8080/api/status
curl http://localhost:8080/metrics

⁠Updating

docker compose:

docker compose pull
docker compose up -d

docker run:

docker pull noxied/wanwatcher:latest
docker stop wanwatcher && docker rm wanwatcher
# re-run your docker run command

See the upgrade guide⁠ for version-specific notes, including the uid 1000 change in 2.0.


⁠Troubleshooting

Container exits immediately

  • Usually a configuration error. Check docker logs wanwatcher for the validation output. Common causes: no notification method enabled, an invalid webhook URL, or a missing required variable.

Permission denied on /data or /logs after upgrading to 2.0

  • The container runs as uid 1000. Run sudo chown -R 1000:1000 ./data ./logs on the host directories.

Notifications not sending

  • Failed sends are retried three times with backoff. Check docker logs wanwatcher | grep -i "notification\|retry" and verify credentials.

IPv6 not detected

  • Confirm the host has IPv6 connectivity and that MONITOR_IPV6="true".

More help: troubleshooting guide⁠.


⁠Building from source

AMD64:

docker build -t wanwatcher:local .

Multi-arch (requires buildx):

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t wanwatcher:local \
  --push .

⁠Tags

TagMeaning
2.5.0This exact release
2.0Latest 2.0.x patch
2Latest 2.x release
latestLatest stable release

⁠Version history

VersionDateHighlights
2.5.02026-06-13Reliability hardening (/healthz staleness, isolated side-effect failures)
2.4.12026-06-13Security: escape untrusted strings in notifications
2.3.02026-06-13AWS Route53 DDNS provider
2.2.02026-06-13Secrets from files, Trivy/SBOM/Cosign supply-chain security
2.1.02026-06-13Optional JSON logging (LOG_FORMAT), adaptive backoff with jitter
2.0.02026-06-11Package rewrite, Apprise, multi-source detection, DDNS, status API and metrics, MQTT and Home Assistant, events, non-root container
1.4.12025-11-02Python 3.14, code quality and security fixes, smaller image
1.4.02025-11-02Configuration validation, notification retry, expanded tests
1.3.02025-10-27Email notifications, update checking, custom avatars
1.2.02025-10-27Telegram support, IPv6 monitoring

Full changelog⁠


⁠Documentation


⁠Security

  • Keep tokens, webhook URLs, and passwords in environment variables, not in images
  • Secrets are never written to the logs
  • Report security issues through the policy linked above

MIT licensed.

Tag summary

Content type

Image

Digest

sha256:c7a77f8af…

Size

50.4 MB

Last updated

4 months ago

docker pull noxied/wanwatcher