Skip to content

Instantly share code, notes, and snippets.

@mehdi89
Last active June 4, 2026 10:13
Show Gist options
  • Select an option

  • Save mehdi89/70ef0664429eec2dd46fa02e4a906c05 to your computer and use it in GitHub Desktop.

Select an option

Save mehdi89/70ef0664429eec2dd46fa02e4a906c05 to your computer and use it in GitHub Desktop.
smokeping-connectivity — one-shot internet disconnect monitor (Docker, Linux/macOS). Graphs latency+loss to Google/Cloudflare/Quad9 every 60s; gaps show when/how long you were offline. Localhost-only dashboard.

smokeping-connectivity

A one-command internet disconnect monitor. It runs SmokePing in Docker and graphs your connection to Google, Cloudflare, and Quad9 (plus your LAN gateway, if detectable) every 60 seconds.

The graphs tell you when your internet dropped and how long it was out — the "smoke" bands show latency jitter and packet loss, and flat gaps mark full outages. The dashboard is bound to localhost only, so it's private by default.

Works on Linux and macOS. The only requirement is Docker.


Install

curl -fsSL https://gist.githubusercontent.com/mehdi89/70ef0664429eec2dd46fa02e4a906c05/raw/smokeping-connectivity.sh | bash

It prints one thing when done:

  Open:  http://127.0.0.1:8559/

Open that URL in your browser. Re-running the command is safe — it just updates the setup.

First data points appear in ~10–20 minutes. The value grows over hours and days, as you build a history of your connection's behavior.


What it monitors

Target Why
8.8.8.8 (Google), 1.1.1.1 (Cloudflare), 9.9.9.9 (Quad9) Internet reachability (WAN). If all go red at once, your uplink/ISP is down.
LAN gateway (auto-detected) Your router. If this goes red too, the problem is local (cable/router). If only the internet anchors fail, it's your ISP.

Reading the graph

  • Colored "smoke" band — latency variance (jitter). Normal.
  • Red on the line — packet loss.
  • Flat gap / 100% loss across all internet targets — you were offline. The x-axis tells you exactly when and for how long.
  • Gap on every graph — the monitor itself was off (machine asleep/rebooted).

Options

Set these as environment variables before the command:

# change the dashboard port
curl -fsSL <RAW_URL> | SMOKEPING_PORT=9000 bash

# expose on your LAN instead of localhost-only (no auth — use with care)
curl -fsSL <RAW_URL> | SMOKEPING_BIND=0.0.0.0 bash
Variable Default Meaning
SMOKEPING_PORT 8559 Local port for the dashboard
SMOKEPING_BIND 127.0.0.1 Bind address; 0.0.0.0 exposes it on your network

Optional: alerts

By default there are no alerts — most people just want to watch the dashboard. If you want to be notified when the internet goes down (and when it recovers), opt in by providing a Telegram bot or any webhook. A tiny sidecar container then checks the internet every 60s and notifies on transitions.

Telegram:

curl -fsSL <RAW_URL> | ALERT_TELEGRAM_TOKEN=123456:abc ALERT_TELEGRAM_CHAT=987654 bash

Generic webhook (Slack, Discord, n8n, your own endpoint — receives a POST with text=...):

curl -fsSL <RAW_URL> | ALERT_WEBHOOK_URL=https://hooks.example/your/path bash
Variable Purpose
ALERT_TELEGRAM_TOKEN + ALERT_TELEGRAM_CHAT Send alerts via a Telegram bot
ALERT_WEBHOOK_URL Send alerts as a POST to any URL

Alerts are edge-triggered (one message on DOWN, one on RECOVERED) with a short debounce, so brief blips won't spam you.


Uninstall

curl -fsSL https://gist.githubusercontent.com/mehdi89/70ef0664429eec2dd46fa02e4a906c05/raw/smokeping-connectivity.sh | bash -s -- --uninstall

Removes the containers but keeps your data (graphs) at ~/.smokeping-connectivity. To wipe everything:

rm -rf ~/.smokeping-connectivity

How it works

  • Uses the maintained lscr.io/linuxserver/smokeping image — identical behavior on Linux and macOS, no per-OS package juggling.
  • Config (Targets, Database) is pre-seeded before first start, so you get a clean monitor (60s interval, just your anchors) with no demo clutter.
  • Config and RRD data live in ~/.smokeping-connectivity/. The container restarts automatically with Docker.
  • Optional alerting runs as a separate curlimages/curl sidecar that checks internet anchors independently — so it still fires even if SmokePing itself has a problem.

Requirements

  • Docker (install). On macOS, Docker Desktop must be running.
  • That's it. SmokePing, fping, RRDtool, and the web server all come inside the image.

Why Docker and not a native install? SmokePing isn't in Homebrew, so there's no clean native macOS path. Docker is the one route that behaves the same on Linux and macOS. (Windows works too under Docker Desktop, though it's not officially covered here yet.)

#!/usr/bin/env bash
#
# smokeping-connectivity — one-shot internet disconnect monitor.
#
# Spins up SmokePing in Docker and graphs your connection to Google / Cloudflare /
# Quad9 (and your LAN gateway, if detectable) every 60 seconds. The "smoke" bands
# show latency variance; flat-line gaps show exactly WHEN and HOW LONG you were
# disconnected. Dashboard is bound to localhost only (private).
#
# Works on Linux and macOS (needs Docker). Re-run any time to update; safe to repeat.
#
# Install (one line) — dashboard only, no alerts:
# curl -fsSL https://gist.githubusercontent.com/mehdi89/70ef0664429eec2dd46fa02e4a906c05/raw/smokeping-connectivity.sh | bash
#
# Options (env vars):
# SMOKEPING_PORT=8559 # localhost port for the dashboard
# SMOKEPING_BIND=127.0.0.1 # set 0.0.0.0 to expose on your LAN (no auth!)
#
# OPTIONAL alerting (default OFF — most people just want the dashboard):
# Enable by setting EITHER a Telegram bot OR any webhook. A tiny sidecar then
# checks the internet every 60s and notifies on DOWN / RECOVERED.
# ALERT_TELEGRAM_TOKEN=123:abc ALERT_TELEGRAM_CHAT=456 # Telegram
# ALERT_WEBHOOK_URL=https://hooks.example/... # OR generic (POST text=...)
# Example:
# curl -fsSL https://gist.githubusercontent.com/mehdi89/70ef0664429eec2dd46fa02e4a906c05/raw/smokeping-connectivity.sh | ALERT_TELEGRAM_TOKEN=xxx ALERT_TELEGRAM_CHAT=yyy bash
#
# Uninstall (removes dashboard + alert sidecar; keeps data):
# curl -fsSL https://gist.githubusercontent.com/mehdi89/70ef0664429eec2dd46fa02e4a906c05/raw/smokeping-connectivity.sh | bash -s -- --uninstall
#
set -euo pipefail
NAME="smokeping-connectivity"
ALERT_NAME="${NAME}-alert"
IMAGE="lscr.io/linuxserver/smokeping:latest"
ALERT_IMAGE="curlimages/curl:latest"
PORT="${SMOKEPING_PORT:-8559}"
BIND="${SMOKEPING_BIND:-127.0.0.1}"
DATADIR="${HOME}/.${NAME}"
c_blue=$'\033[1;36m'; c_grn=$'\033[1;32m'; c_red=$'\033[1;31m'; c_dim=$'\033[2m'; c_off=$'\033[0m'
log(){ printf "%s[smokeping]%s %s\n" "$c_blue" "$c_off" "$*" >&2; }
ok(){ printf "%s[smokeping]%s %s\n" "$c_grn" "$c_off" "$*" >&2; }
die(){ printf "%s[smokeping]%s %s\n" "$c_red" "$c_off" "$*" >&2; exit 1; }
# ---- uninstall -------------------------------------------------------------
if [ "${1:-}" = "--uninstall" ]; then
log "Removing containers..."
docker rm -f "$NAME" "$ALERT_NAME" >/dev/null 2>&1 || true
log "Removed. Data kept at: $DATADIR"
log "Delete data too with: rm -rf \"$DATADIR\""
exit 0
fi
# ---- preflight -------------------------------------------------------------
command -v docker >/dev/null 2>&1 || die "Docker not found. Install Docker first: https://docs.docker.com/get-docker/"
docker info >/dev/null 2>&1 || die "Docker is installed but not running. Start Docker (Desktop) and re-run."
# ---- detect default gateway (best effort) ----------------------------------
detect_gateway(){
local gw=""
case "$(uname -s)" in
Linux) gw=$(ip route 2>/dev/null | awk '/^default/{print $3; exit}') ;;
Darwin) gw=$(route -n get default 2>/dev/null | awk -F': ' '/gateway/{print $2; exit}' | tr -d ' ')
[ -z "$gw" ] && gw=$(netstat -rn -f inet 2>/dev/null | awk '$1=="default"{print $2; exit}') ;;
esac
printf '%s' "$gw" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' && printf '%s' "$gw" || printf ''
}
GW="$(detect_gateway || true)"
# ---- write config (pre-seed BEFORE first start = clean, no demo targets) ----
log "Writing config to $DATADIR ..."
mkdir -p "$DATADIR/config" "$DATADIR/data"
cat > "$DATADIR/config/Database" <<'EOF'
*** Database ***
step = 60
pings = 20
# consfn mrhb steps total
AVERAGE 0.5 1 28800
AVERAGE 0.5 12 9600
MIN 0.5 12 9600
MAX 0.5 12 9600
AVERAGE 0.5 144 2400
MAX 0.5 144 2400
MIN 0.5 144 2400
EOF
{
echo "*** Targets ***"
echo "probe = FPing"
echo "menu = Top"
echo "title = Connectivity Monitor"
echo "remark = Internet disconnect tracking. Gaps = offline, smoke = jitter/loss."
echo ""
echo "+ Internet"
echo "menu = Internet"
echo "title = Internet Reachability (WAN)"
echo ""
echo "++ GoogleDNS"
echo "menu = Google DNS"
echo "title = Google DNS (8.8.8.8)"
echo "host = 8.8.8.8"
echo ""
echo "++ Cloudflare"
echo "menu = Cloudflare"
echo "title = Cloudflare (1.1.1.1)"
echo "host = 1.1.1.1"
echo ""
echo "++ Quad9"
echo "menu = Quad9"
echo "title = Quad9 (9.9.9.9)"
echo "host = 9.9.9.9"
if [ -n "$GW" ]; then
echo ""
echo "+ Local"
echo "menu = Local"
echo "title = Local Network (LAN)"
echo ""
echo "++ Gateway"
echo "menu = Gateway"
echo "title = Local Gateway ($GW)"
echo "host = $GW"
fi
} > "$DATADIR/config/Targets"
[ -n "$GW" ] && log "Gateway detected: $GW (added as a target)" || log "No LAN gateway detected — monitoring internet anchors only."
# ---- (re)launch dashboard container ----------------------------------------
log "Pulling image (first run only)..."
docker pull "$IMAGE" >/dev/null 2>&1 || log "Pull skipped/failed; using local image if present."
docker rm -f "$NAME" >/dev/null 2>&1 || true
log "Starting SmokePing on ${BIND}:${PORT} ..."
docker run -d \
--name "$NAME" \
--restart unless-stopped \
-p "${BIND}:${PORT}:80" \
-e PUID="$(id -u)" -e PGID="$(id -g)" \
-e TZ="$( (cat /etc/timezone 2>/dev/null) || echo Etc/UTC)" \
-v "$DATADIR/config:/config" \
-v "$DATADIR/data:/data" \
"$IMAGE" >/dev/null
# ---- wait until it serves ---------------------------------------------------
URL="http://${BIND}:${PORT}/"
log "Waiting for dashboard..."
for _ in $(seq 1 30); do
docker exec "$NAME" sh -c 'wget -q -O /dev/null http://127.0.0.1:80/ 2>/dev/null' && { ok "Up and collecting."; break; }
sleep 2
done
# ---- OPTIONAL alerting sidecar (only if user opted in) ----------------------
docker rm -f "$ALERT_NAME" >/dev/null 2>&1 || true
ATOKEN="${ALERT_TELEGRAM_TOKEN:-}"; ACHAT="${ALERT_TELEGRAM_CHAT:-}"; AWEBHOOK="${ALERT_WEBHOOK_URL:-}"
if [ -n "$ATOKEN$AWEBHOOK" ]; then
log "Alerting: ENABLED — checking internet every 60s, notify on DOWN/RECOVERED."
cat > "$DATADIR/alert-loop.sh" <<'ALOOP'
#!/bin/sh
# Checks a few internet anchors every 60s; edge-triggered notify on DOWN/RECOVERED.
ANCHORS="https://1.1.1.1 https://8.8.8.8 https://dns.google"
STATE=UP; fails=0
notify(){
[ -n "$TG_TOKEN" ] && curl -s --max-time 15 "https://api.telegram.org/bot$TG_TOKEN/sendMessage" \
--data-urlencode "chat_id=$TG_CHAT" --data-urlencode "text=$1" >/dev/null 2>&1
[ -n "$WEBHOOK" ] && curl -s --max-time 15 "$WEBHOOK" --data-urlencode "text=$1" >/dev/null 2>&1
}
while true; do
up=0
for a in $ANCHORS; do curl -s -o /dev/null --max-time 8 "$a" && { up=1; break; }; done
TS=$(date -u "+%Y-%m-%d %H:%M:%SZ")
if [ "$up" = 1 ]; then
[ "$STATE" = DOWN ] && notify "RECOVERED: internet back on ${HOSTLABEL} at ${TS}."
STATE=UP; fails=0
else
fails=$((fails+1))
if [ "$STATE" != DOWN ] && [ "$fails" -ge 2 ]; then
notify "DOWN: internet unreachable on ${HOSTLABEL} (all anchors failed) at ${TS}."
STATE=DOWN
fi
fi
sleep 60
done
ALOOP
docker run -d \
--name "$ALERT_NAME" \
--restart unless-stopped \
-e TG_TOKEN="$ATOKEN" -e TG_CHAT="$ACHAT" -e WEBHOOK="$AWEBHOOK" \
-e HOSTLABEL="$(hostname)" \
-v "$DATADIR/alert-loop.sh:/alert-loop.sh:ro" \
--entrypoint /bin/sh "$ALERT_IMAGE" /alert-loop.sh >/dev/null
else
log "${c_dim}Alerting: disabled. Enable with ALERT_TELEGRAM_TOKEN+ALERT_TELEGRAM_CHAT or ALERT_WEBHOOK_URL.${c_off}"
fi
# ---- done: print the URL ----------------------------------------------------
printf "\n"
ok "SmokePing connectivity monitor is running."
log "${c_dim}First data points appear in ~10-20 min; value grows over hours/days.${c_off}"
log "${c_dim}Logs: docker logs -f $NAME${c_off}"
log "${c_dim}Stop/rm: curl -fsSL https://gist.githubusercontent.com/mehdi89/70ef0664429eec2dd46fa02e4a906c05/raw/smokeping-connectivity.sh | bash -s -- --uninstall${c_off}"
printf "\n %sOpen:%s %s\n\n" "$c_grn" "$c_off" "$URL"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment