wxalerts-meshmonitor is a one-container bridge. It subscribes to the counties
you name on the public WxAlerts MQTT feed, decides which hazards are worth
airtime, compresses each one to a single line that fits a radio packet, and
hands it to MeshMonitor to transmit.
wss://mqtt.wxalerts.org/mqtt ──▶ wxalerts-meshmonitor ──▶ POST /api/messages/send ──▶ LoRa
retained, county-routed filter, squeeze, ration MeshMonitor
What arrives on the mesh:
🌪 TORNADO WARNING Santa Rosa, Baldwin til 3:45p TAKE COVER NOW!
What you need
- A running MeshMonitor with at least one source attached, whether Meshtastic or MeshCore both work.
- An API token from that MeshMonitor, belonging to a user with write permission on whichever channel you intend to transmit on.
- A host that can reach both
mqtt.wxalerts.organd MeshMonitor. A Pi is plenty, and it does not have to be the node itself.
Published images are linux/amd64 and linux/arm64. There is deliberately no
32-bit ARM build, so a Pi running the older 32-bit Raspberry Pi OS cannot pull
the image, so reimage to 64-bit or run from source.
1. Get a MeshMonitor API token
In MeshMonitor: User Settings → API Token → Generate. It looks like
mm_v1_… and is shown once. The token inherits its user’s permissions, so that
user needs write on the channel your rules target.
2. Generate a config
Nobody knows their county’s six-digit SAME code offhand, so the init
subcommand resolves it from a latitude and longitude:
docker run --rm ghcr.io/wxalerts/wxalerts-meshmonitor \
init --lat 30.6103 --lon -87.0547 > wxalerts.yml
That asks api.weather.gov which county zone contains the point, converts the
UGC zone to its SAME code, fills in the county name, timezone and local
forecast office, and writes a config with the phenomenon codes explained in
comments. It only covers the United States and its territories.
The -o <file> flag writes to a file instead of stdout, and refuses to
overwrite one that already exists.
3. Run it
# docker-compose.yml
services:
wxalerts:
image: ghcr.io/wxalerts/wxalerts-meshmonitor:latest
restart: unless-stopped
environment:
MESHMONITOR_TOKEN: ${MESHMONITOR_TOKEN}
volumes:
- ./wxalerts.yml:/config/wxalerts.yml:ro
ports:
# /healthz, /stats, /live and Prometheus /metrics. Only needed if you
# scrape it or are debugging.
- "8092:8092"
The config mounts read-only because the bridge holds no state on disk. The retained MQTT feed is the state store, and it is re-read on every connect.
If MeshMonitor runs in the same compose project, meshmonitor.url is its
service name, http://meshmonitor:8080, not localhost. The repo ships a
docker-compose.example.yml with both services side by side.
4. Leave dry_run on for a day
A freshly generated config has delivery.dry_run: true. In that mode every
message is selected, rendered and logged exactly as it would be sent, and
nothing is transmitted:
2026-08-17 14:22:03 INFO wxmesh.bridge: queued [warnings p80 ch2 87B] ⛈ SEVERE THUNDERSTORM WARNING Santa Rosa til 3:15p Move indoors.
2026-08-17 14:22:03 INFO wxmesh.bridge: [dry-run] ch2: ⛈ SEVERE THUNDERSTORM WARNING Santa Rosa til 3:15p Move indoors.
Watch docker logs -f through one round of real weather. That is how you find
out whether your rules are too loud, before your neighbours do. Then set
dry_run: false and restart.
Startup, in order
The first three lines of the log say which of the three common mistakes you made, because all of them otherwise look identical to a quiet weather day:
- The config is validated in full before anything connects. A bad SAME code, an unknown timezone or a rule with no channel exits immediately.
- MeshMonitor reachability, via
GET /api/health, which needs no auth. A failure here is usually the URL. - The token, via
GET /api/v1/sources, which does. A 401 or 403 here means regenerate it.
Failing 2 or 3 is not fatal: the bridge keeps running and retries, because a token fixed in the MeshMonitor UI should take effect without a restart. A rule naming a channel that MeshMonitor genuinely does not have is fatal: it exits with the list of channels that do exist, because that will never fix itself and crash-looping is the fastest way to get a typo noticed.
Where to go next
- Configuration reference: every key, the rule language, and the message templates.
- What it decides to send: the decision path, escalation, and the airtime budget.
- Monitoring and troubleshooting: the health endpoints, what to alert on, and what goes wrong.
