A lightweight Go binary that executes monitoring checks on behalf of an EasyMonitor server. Probes pull work from the server's Redis Streams, run HTTP/ICMP checks, and publish results back.
Deploy one or more probes across different regions/networks. The server applies cross-probe quorum so alerts only fire when a majority of probes agree.
Pre-built images are published for every release:
- Docker Hub:
easymonitor/probe-node:latest - GitHub Container Registry:
ghcr.io/easymonitordev/probe-node:latest
Both are multi-arch (linux/amd64, linux/arm64) and built from the same source.
- An EasyMonitor server already running (main repo)
- A probe JWT token generated on that server
- The server's Redis URL and password (from its
.env) - A network path from this host to the server's Redis — typically via Tailscale or Cloudflare Tunnel. Never expose Redis over plaintext on the public internet.
Generate a token on the EasyMonitor server:
docker compose exec php php artisan probe:generate-token \
--node-id=us-east-1 \
--tags=us-east-1,production \
--expires=365Copy the printed token.
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale upJoin the same tailnet your EasyMonitor server is on. Verify:
ping -c 2 <server-tailscale-ip>docker run -d \
--name easymonitor-probe \
--restart unless-stopped \
--network host \
-e NODE_ID="us-east-1" \
-e REDIS_URL="redis://<server-tailscale-ip>:6379/0" \
-e REDIS_PASSWORD="<redis password from server .env>" \
-e JWT_TOKEN="<probe token from step above>" \
easymonitor/probe-node:latest--network host is required so the container shares the host's tailscale0
interface — without it, the container's own network namespace can't reach
the tailnet. Port 8080 is automatically exposed on the host, no -p needed.
curl -s http://localhost:8080/health
# {"status":"healthy","node_id":"us-east-1"}On the EasyMonitor server, confirm the probe registered:
docker compose exec redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning \
XINFO GROUPS checksYou should see a consumer group named probe-us-east-1.
- Server side:
cloudflared tunnel create+ingressconfig exposingtcp://localhost:6379under a hostname - Probe side:
cloudflared access tcp --hostname=... --url=127.0.0.1:6379+ Cloudflare service token - Run probe with
--network hostandREDIS_URL=redis://127.0.0.1:6379/0
Full instructions are in the main repo's PROBE_NODE_SETUP.md.
Any private network path works. Point REDIS_URL at the appropriate private address.
ssh -L 6379:127.0.0.1:6379 -N user@server &
docker run -d \
--name easymonitor-probe \
--restart unless-stopped \
--network host \
-e NODE_ID="eu-west-1" \
-e REDIS_URL="redis://127.0.0.1:6379/0" \
-e REDIS_PASSWORD="..." \
-e JWT_TOKEN="..." \
easymonitor/probe-node:latest| Variable | Required | Default | Purpose |
|---|---|---|---|
NODE_ID |
yes | — | Unique identifier for this probe (e.g. us-east-1) |
REDIS_URL |
yes | — | redis://host:port/db |
REDIS_PASSWORD |
yes (if server has one) | — | Must match the server's REDIS_PASSWORD |
JWT_TOKEN |
yes | — | Probe auth token generated by the server |
DEFAULT_TIMEOUT |
no | 30s |
Default per-check timeout |
BATCH_SIZE |
no | 10 |
Max checks pulled per XREADGROUP |
MAX_CONCURRENCY |
no | 10 |
Concurrent in-flight checks |
HEALTH_CHECK_PORT |
no | 8080 |
HTTP port for /health, /ready, /version |
REDIS_DB |
no | 0 |
Redis database number |
GET /health— 200 if the probe is consuming, 503 otherwiseGET /ready— same semantics as/healthGET /version— build version and timestamp
┌──────────────────────────┐
│ EasyMonitor server │
│ (Laravel + Redis) │
└──────────┬───────────────┘
│
private tunnel │ (Tailscale / Cloudflare / VPN)
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ probe │ │ probe │ │ probe │
│ us-east │ │ eu-west │ │ ap-south │
└──────────┘ └──────────┘ └──────────┘
Each probe uses a unique Redis Streams consumer group (probe-<NODE_ID>) so every check is delivered to every probe. The server groups per-probe results by a round_id and decides monitor status by majority vote.
Requires Go 1.24+.
git clone https://github.com/easymonitordev/probe-node.git
cd probe-node
make build
./bin/probe-nodemake build-linux # linux/amd64
make build-linux-arm # linux/arm64make test # -race -coverprofile=coverage.out
make test-coverage # generate coverage.htmlmake docker-build # single-arch
make docker-build-multiarch # amd64 + arm64 via buildxCreate /etc/systemd/system/easymonitor-probe.service:
[Unit]
Description=EasyMonitor Probe
After=network.target
[Service]
Type=simple
User=probe
EnvironmentFile=/etc/easymonitor/probe.env
ExecStart=/usr/local/bin/probe-node
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target/etc/easymonitor/probe.env:
NODE_ID=us-east-1
REDIS_URL=redis://100.x.y.z:6379/0
REDIS_PASSWORD=...
JWT_TOKEN=...sudo systemctl enable --now easymonitor-probeA minimal manifest:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: easymonitor-probe
spec:
serviceName: probe
replicas: 1
selector:
matchLabels:
app: easymonitor-probe
template:
metadata:
labels:
app: easymonitor-probe
spec:
containers:
- name: probe
image: easymonitor/probe-node:latest
env:
- name: NODE_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: easymonitor-probe
key: redis_url
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: easymonitor-probe
key: redis_password
- name: JWT_TOKEN
valueFrom:
secretKeyRef:
name: easymonitor-probe
key: jwt_token
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080Tokens should be rotated periodically (every 90–365 days recommended). On the server:
docker compose exec php php artisan probe:generate-token \
--node-id=us-east-1 \
--expires=365Update the token on the probe and restart the container.
"AUTH failed" in logs — REDIS_PASSWORD doesn't match the server. Check it's copied exactly from the server's .env.
"failed to validate token" — token expired or was generated with a different JWT_SECRET. Regenerate on the server.
Probe starts but no checks run — confirm Horizon is running on the server and that there are active monitors. Also verify the probe shows up: docker compose exec redis redis-cli -a "$REDIS_PASSWORD" XINFO GROUPS checks.
Can't reach Redis — verify the tunnel: tailscale status / systemctl status cloudflared. Test connectivity: redis-cli -h <host> -a "$REDIS_PASSWORD" ping should return PONG.
See CONTRIBUTING.md for the development setup and PR guidelines.
MIT — see LICENSE.
Part of the EasyMonitor project.