Schedule Ookla speed tests across every connection, track download / upload / ping / packet-loss trends, get alerts, and export to Prometheus & Grafana — all on your own Docker host.
Documentation · Getting started · How it works · Contributing
NetPulse is a self-hosted, metrics-first internet speed tracker. Instead of running a
one-off speed test in your browser, NetPulse continuously measures your real ISP performance over
time: it schedules Ookla Speedtest runs across one or more
connections, records every result, and turns them into live charts, a searchable history, a
weekday × hour heatmap, alerts, and a Prometheus /metrics endpoint you can graph in Grafana.
It runs entirely on Docker, is single-tenant (one admin, your data only), and is built on PHP 8.5 / Symfony 8.1 with a clean Hexagonal + DDD architecture.
- 📈 Metrics-first — every measurement on a Prometheus
/metricsendpoint + a bundled Grafana dashboard (optional remote-write). - 🛰️ Scheduled & adaptive testing — even or cron schedules per connection, round-robin server selection, and faster retesting of a degraded link.
- 🌐 Multi-connection — monitor several WANs/links independently, each with its own schedule, server pool and thresholds.
- 📊 Dashboard, history & heatmap — live Speed / Ping / Loss charts (24h–90d), filterable history with CSV export, and a "when is it slow?" heatmap.
- 🔔 Alerts & digests — edge-debounced alert/recovery notifications over email, chat or webhook, plus a daily/weekly digest.
- 🔒 Private & secure — single admin created on first run, optional OIDC SSO and TOTP 2FA, strict CSP, token-guarded agent API.
- 🐳 Self-hosted — Docker Compose for the server, Prometheus, Grafana, and the probe agent. No third-party telemetry.
- 🧩 Clean & strict — Hexagonal + DDD enforced by deptrac, Mago (lint/format/analyze), a no-Node frontend (AssetMapper + Tailwind + Alpine + uPlot).
The live dashboard — per-connection health, speed / ping / loss charts, trend tiles and sparklines:
![]() |
![]() |
![]() |
![]() |
More in the screenshots gallery.
NetPulse is pull-based: lightweight probe agents poll the server for due work, run the Ookla CLI, and push results back. The server decides when a connection is due (there is no server-side cron) and fans each result out to the dashboard, history, metrics and notifications.
flowchart LR
Operator((Operator)) -->|login| WEB[Web dashboard]
AG[Probe agent] -->|run| OK[Ookla CLI]
AG -->|poll /due, POST /results| API[Agent API /api/v1]
WEB --- DB[(SQLite / PostgreSQL)]
API --- DB
MET["/metrics"] --- DB
PROM[Prometheus] -->|scrape| MET
PROM --> GRAF[Grafana]
Read the full walkthrough — with the agent loop, the due-decision flow, and the architecture — in the documentation.
Prerequisites: Docker + Docker Compose, git, and
just(brew install just, or use the Nix shell — it providesjust; see Getting started for how to install Nix and enter the shell).
git clone https://github.com/MrHDOLEK/NetPulse.git
cd NetPulse
just install # build the image, start containers, composer install
# Build the database from migrations
docker compose exec -T app composer migrateDevelop on the host with Nix (instead of Docker for the PHP toolchain): install Nix once —
sh <(curl -L https://nixos.org/nix/install)(instructions) — then runnix-shellfrom the project root to get PHP 8.5 + Composer + Mago + Symfony CLI +just+ the OoklaspeedtestCLI:nix-shell # enter the dev shell nix-shell --pure # full isolation from the host nix-shell --arg php-version 8.4 # pick a PHP version (default: 8.5)More in Getting started → Develop with Nix.
Open http://localhost:8080 — every page redirects to /setup until you create the first
admin (email + password, min 12 chars). Then add a probe and a connection, and start the agent:
# 1. Create a probe (the token is shown once)
docker compose exec -T app php bin/console app:probe:create "Office probe"
# 2. Create a connection for it (288 tests/day, round-robin over 2 servers)
docker compose exec -T app php bin/console app:connection:create "WAN" \
--probe=<PROBE_ID> --schedule-mode=even --tests-per-day=288 --server-pool=11111,22222
# 3. Run the agent (it polls for due work and runs speedtests)
PROBE_ID=<PROBE_ID> PROBE_TOKEN=<PROBE_TOKEN> docker compose --profile agent up agentMeasurements now flow into the dashboard at http://localhost:8080, the REST API docs at
http://localhost:8080/api/doc, and the Prometheus /metrics endpoint.
👉 Full instructions: Getting started.
For production there's a single self-contained image — nginx + php-fpm in one container (managed
by supervisord), with the app code, dependencies and compiled assets all baked in. The only state is
one var/ volume. No bind mounts, no separate web server.
The image is published to the GitHub Container Registry (ghcr.io/mrhdolek/netpulse) on every
push to main. Pull and run it:
# a stable random secret (rotating it logs everyone out — keep it)
export APP_SECRET=$(openssl rand -hex 16)
docker run -d --name netpulse \
-p 8080:8080 \
-e APP_SECRET="$APP_SECRET" \
-v netpulse_var:/var/www/var \
ghcr.io/mrhdolek/netpulse:latest…or with the bundled compose file (same image, plus a named volume):
export APP_SECRET=$(openssl rand -hex 16)
docker compose -f compose.prod.yml pull
docker compose -f compose.prod.yml up -dEither way it runs database migrations on boot and serves the dashboard on
http://localhost:8080 (create the first admin at /setup). It defaults to SQLite in the volume;
point DATABASE_URL at PostgreSQL for larger installs.
Prefer to build from source? docker compose -f compose.prod.yml up -d --build, or
docker build -f .docker/php/Dockerfile --target prod -t netpulse ..
compose.prod.yml also ships an agent service (the same image, behind the agent profile), so
a probe can run alongside the server on one instance — it reads everything it needs from env. Once
the server is up, create a probe and feed its identity to the agent:
# create a probe inside the running server (token shown once)
docker compose -f compose.prod.yml exec netpulse php bin/console app:probe:create "local"
# put PROBE_ID + PROBE_TOKEN in your .env, then start the agent:
docker compose -f compose.prod.yml --profile agent up -dThe agent polls the server at http://netpulse:8080 over the compose network and runs speedtest
for each due task. Knobs (all env): PROBE_ID, PROBE_TOKEN, AGENT_POLL_INTERVAL, OOKLA_BINARY.
To start it together with the stack every time, set COMPOSE_PROFILES=agent in your .env.
The agent has a healthcheck (the server's per-probe liveness endpoint, which reports 503 if the
probe stops polling) and an autoheal sidecar (also under the agent profile) that restarts the
agent container when it goes unhealthy.
The complete docs live at mrhdolek.github.io/NetPulse:
| Page | What's in it |
|---|---|
| Introduction | What NetPulse is and the core concepts. |
| Getting started | Install and first-run walkthrough. |
| How it works | The agent loop, scheduling, and data flow (with diagrams). |
| Configuration | Every environment variable and option. |
| Architecture | Hexagonal + DDD layers and modules. |
| Contributing | Dev setup, the quality gate, and conventions. |
The docs site is built with VitePress and lives in docs/. To run it locally:
cd docs && npm install && npm run docs:devPHP 8.5 · Symfony 8.1 · Doctrine ORM (XML mapping, migrations-only) · SQLite / PostgreSQL ·
Ookla Speedtest CLI · Prometheus + Grafana · Twig + Tailwind 3 + Alpine.js (CSP build) + uPlot via
AssetMapper (no Node) · Hexagonal + DDD (deptrac) · Mago (lint/format/analyze) · PHPUnit.
A host Nix shell (shell.nix) provides PHP 8.5 + Composer + Mago + Symfony CLI + just + the Ookla speedtest CLI — enough to serve the app (symfony server:start) and run the agent (app:agent:run) on the host.
Contributions are welcome! Please read the contributing guide,
keep the quality gate green, and
start PR titles with - (the CI-enforced convention, e.g. - Add X). Found a bug or have
an idea? Open an issue.
Released under the MIT License.




