speedtest-monitor

speedtest-monitor

Docker app from GreiteTurtle's Repository

Overview

Self-hosted internet speed monitor. Runs Cloudflare Speed Test on a configurable schedule, stores every measurement in SQLite, and serves a dashboard with live updates over WebSocket. Includes alerts (webhook, ntfy, Discord, Slack, SMTP), authentication (local accounts and OIDC), and role-based access (admin / viewer). On first boot, set AUTH_SECRET (required) and BETTER_AUTH_URL (required when accessed behind a reverse proxy). Either set SPEEDTEST_ADMIN_EMAIL + SPEEDTEST_ADMIN_PASSWORD to seed the first admin, or visit /setup to create it from the UI. All other variables are optional. Data is persisted to /data (mapped to /mnt/user/appdata/speedtest-monitor by default).

Speedtest Monitor

Self-hosted internet speed monitor. Runs @cloudflare/speedtest on a configurable schedule, stores every measurement in SQLite, and serves a dashboard with live updates over WebSocket.

Dashboard

Stack

  • Runtime: Bun 1.x
  • Package manager: Bun 1.3
  • Framework: Next.js 16 (App Router) + TypeScript 6 + Biome 2 + Tailwind v4
  • Custom server (server.ts): hosts Next.js and a ws WebSocket endpoint on the same port
  • Scheduler: node-cron 4.x, reprogrammable at runtime from the UI
  • DB: Drizzle ORM 0.45 + bun:sqlite (built-in)
  • Measurement engine: @cloudflare/speedtest (HTTP-only, no browser)
  • Tests: Bun native (bun test)
  • Runtime image: oven/bun:1-slim

Run with Docker

docker compose up -d --build
open http://localhost:3000

Configuration:

Variable Default Purpose
SPEEDTEST_INTERVAL_MINUTES 15 Default interval used if the DB has no override
SPEEDTEST_DB_PATH /data/speedtest.db SQLite file path (volume-persisted)
SPEEDTEST_TEST_DURATION_S 10 Duration of each download/upload phase (seconds). Raise for multi-Gbps links so TCP can ramp up and the sample is not dominated by slow-start.
SPEEDTEST_PARALLEL_STREAMS 8 Parallel HTTP streams per phase. More streams help saturate high-bandwidth links (try 16 on 10 Gbps).
SPEEDTEST_LOCALE en-US BCP 47 locale for dates/times in the dashboard and alert emails (e.g. fr-FR, de-DE)
SPEEDTEST_TIMEZONE UTC IANA timezone for dates/times in the dashboard and alert emails (e.g. Europe/Paris)
PORT 3000 HTTP + WebSocket port
TZ - Container system timezone (log lines only; display uses SPEEDTEST_TIMEZONE)

Change the interval at any time via the UI (/settings) - it is persisted in SQLite and takes effect immediately without a restart.

Alerts

speedtest-monitor can notify you when your connection degrades or fails, and again when it recovers. Alerts are fully opt-in - nothing fires until you enable them and set at least one threshold.

Configure destinations (env vars)

All destinations are optional. A destination is available iff its required env vars are set.

Destination Required env Optional env
Webhook (generic) SPEEDTEST_WEBHOOK_URL SPEEDTEST_WEBHOOK_HEADERS (JSON)
ntfy SPEEDTEST_NTFY_URL (full URL incl. topic) SPEEDTEST_NTFY_TOKEN
Discord SPEEDTEST_DISCORD_WEBHOOK -
Slack SPEEDTEST_SLACK_WEBHOOK -
Email (SMTP) SPEEDTEST_SMTP_HOST, SPEEDTEST_SMTP_FROM, SPEEDTEST_SMTP_TO SPEEDTEST_SMTP_PORT (587), SPEEDTEST_SMTP_SECURE (auto), SPEEDTEST_SMTP_USER, SPEEDTEST_SMTP_PASS
- - SPEEDTEST_PUBLIC_URL (dashboard link in emails)

Configure rules (UI)

In /settings, toggle "Enable alerts", set any subset of thresholds (download, upload, latency, bufferbloat, failure streak), and enable the destinations you want. Each destination has a "Send test" button to verify wiring without waiting for a real incident.

Semantics

  • Fire on transition only. Once a condition enters ALERTING it will not re-fire until it first recovers. No spam during prolonged outages.
  • Per-condition state. Download dropping while latency is fine -> one alert. Latency spiking later -> a second independent alert.
  • Failure streak. Counts consecutive measurements with status error/timeout ending at the current one - useful when the connection actually goes down.
  • Secrets stay in env. Destination credentials are never persisted to the SQLite DB and never returned by /api/alerts/rules.

Authentication

speedtest-monitor requires authentication for all routes. Two roles exist:

  • admin - full access, can manage users, change settings, configure alerts, trigger manual measurements
  • viewer - read-only access: dashboard, history, settings in read-only mode

Required environment

AUTH_SECRET=<run: openssl rand -base64 32>
BETTER_AUTH_URL=https://speedtest.example.com   # required when served behind a reverse proxy

Missing AUTH_SECRET is a fatal boot error. BETTER_AUTH_URL defaults to http://localhost:${PORT} when unset, which is fine for local dev but must be overridden in production so callbacks and OIDC redirects work.

First run: create the first admin

Pick one of the following three paths. They are independent; the most recent write wins.

Path A: env seed (recommended for docker-compose)

SPEEDTEST_ADMIN_EMAIL=admin@example.com
SPEEDTEST_ADMIN_PASSWORD=<at least 10 chars>

The seed runs at every boot and is idempotent: the admin is created if absent, promoted to admin if demoted, and rehashed if the password env var has changed.

Path B: setup wizard

Visit /setup on first boot. The page is only accessible while no user exists. You can create your admin account from the form.

Path C: OIDC admin claim

Set SPEEDTEST_OIDC_ADMIN_EMAIL=you@example.com in addition to the OIDC env vars below. On first OIDC sign-in with that email, the account is auto-promoted to admin.

OIDC single sign-on (optional)

SPEEDTEST_OIDC_ISSUER=https://auth.example.com
SPEEDTEST_OIDC_CLIENT_ID=speedtest
SPEEDTEST_OIDC_CLIENT_SECRET=...
SPEEDTEST_OIDC_DISPLAY_NAME=SSO           # label on the sign-in button
SPEEDTEST_OIDC_ADMIN_EMAIL=you@example.com
SPEEDTEST_OIDC_ALLOW_NEW_USERS=true       # "false" = only admin-created users may sign in via OIDC

Tested with Authelia, Authentik, and Keycloak (any OIDC-compliant provider should work).

User management

Admins manage users from /settings under the "Users" card. Add a user with an initial password (no email invite in v1). Admins can change roles, reset passwords, and delete users. The last remaining admin cannot be demoted or deleted.

Upgrading from a pre-auth version

  1. Set AUTH_SECRET in your environment before starting the upgraded container.
  2. Pick a bootstrap path (A, B, or C above).
  3. Scripts and cURL commands that previously called /api/* anonymously will now receive 401 Unauthorized. Add an authenticated session cookie or migrate to the session-aware clients.

Upgrading from the next-auth release

The auth stack moved from next-auth v5 beta to Better Auth. Same env vars, but:

  1. Set BETTER_AUTH_URL to your public origin (AUTH_TRUST_HOST is no longer used).
  2. On first boot, the existing users table is copied into the new user/account tables. Argon2id password hashes verify without re-hashing.
  3. Sessions are now stored in the database (no longer JWT), so every user must sign in again after the upgrade.
  4. User ids changed from auto-increment integers to UUIDs. Any external script that hard-coded a numeric user id needs to be updated.

Upgrading the measurement engine

The 0.2+ release swaps the measurement backend from fast.com (via the fast-cli + Chromium browser) to Cloudflare Speed Test (HTTP only). On first boot after the upgrade:

  • Three new nullable columns (jitter_ms, packet_loss_pct, user_isp) are added to the measurements table. Historical rows keep them null - the data was never captured.
  • Post-upgrade rows record the Cloudflare edge code (e.g. CDG) in server_locations instead of Netflix Fast's location strings (e.g. Paris, FR | Saint Denis, FR).
  • Absolute speed numbers may differ slightly vs. pre-upgrade runs because the CDN behind the test is different. The trend is still directly comparable.
  • The container image shrinks by roughly 700 MB; no Chromium, no fonts-liberation, no sandbox requirement.

Development

bun install
bun run db:generate        # generate drizzle migrations
bun run dev                # bun server.ts -> http://localhost:3003
bun test                   # bun native unit tests
bun run lint               # biome check .
bun run tsc                # tsc --noEmit
bun run build              # drizzle generate + fetch releases + build email templates + next build

End-to-end check

# trigger one measurement (~20s)
curl -X POST http://localhost:3003/api/measurements/run

# read history
curl 'http://localhost:3003/api/measurements?range=24h' | jq

# tail the WebSocket stream
websocat ws://localhost:3003/ws

# change interval (live)
curl -X PATCH http://localhost:3003/api/settings \
  -H 'content-type: application/json' \
  -d '{"intervalMinutes":5}'

API

All routes require an authenticated session (cookie or OIDC). Mutating routes require the admin role.

Route Method Description
/api/health GET Liveness probe (no auth)
/api/measurements?range=6h|12h|24h|7d|30d GET History for the given range
/api/measurements/table GET Paginated history with server-side sort/filter (used by the dashboard table)
/api/measurements/run POST Trigger a manual run (409 if already running)
/api/settings GET Current interval + env default
/api/settings PATCH { intervalMinutes: 1..1440 }
/api/alerts/rules GET / PUT Read or upsert the alert rule set
/api/alerts/test POST Send a synthetic alert to a destination to verify wiring
/api/users GET / POST List users / create a user (admin)
/api/users/[id] PATCH / DELETE Update role or delete a user (admin)
/api/users/[id]/reset-password POST Reset a user's password (admin)
/api/account/password PATCH Change your own password
/api/auth/setup POST First-run admin creation (only while no user exists)
/api/auth/[...all] * Better Auth endpoints (sign-in, callback, session, ...)
/ws WebSocket Pushes measurement, running, settings_updated events

Tests

Unit tests live next to the code they cover (lib/**/*.test.ts, components/**/*.test.ts). Run with:

bun test

Covered areas: cron expression generation, measurement DTO mapping, range and pagination query parsing, alert evaluation and per-condition state machine, alert dispatch and formatting, MJML email rendering, every destination adapter (webhook, ntfy, Discord, Slack, SMTP), auth bootstrap and config, password hashing, OIDC providers, and user management.

Reverse proxy

The WebSocket lives on /ws. Make sure your proxy forwards the Upgrade header:

location /ws {
  proxy_pass http://speedtest:3000;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
}

Media gallery

Install speedtest-monitor on Unraid in a few clicks.

Find speedtest-monitor in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for speedtest-monitor Review the template variables and paths Click Install

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/greite/speedtest-monitor:latest
Last Updated2026-07-17
First Seen2026-05-12

Runtime arguments

Web UI
http://[IP]:[PORT:3000]/
Network
bridge
Shell
sh
Privileged
false

Template configuration

WebUI PortPorttcp

HTTP + WebSocket port served by the app.

Target
3000
Default
3000
Value
3000
App DataPathrw

Persistent storage for the SQLite database (speedtest.db).

Target
/data
Default
/mnt/user/appdata/speedtest-monitor
Value
/mnt/user/appdata/speedtest-monitor
AUTH_SECRETVariable

REQUIRED. Secret used to sign session cookies. Generate with: openssl rand -base64 32

BETTER_AUTH_URLVariable

Public URL of the dashboard, used by Better Auth for sign-in callbacks and OIDC redirects (e.g. https://speedtest.example.com). REQUIRED when running behind a reverse proxy. Defaults to http://localhost:PORT if unset - fine for direct local access only.

SPEEDTEST_INTERVAL_MINUTESVariable

Default measurement interval in minutes (1-1440). Can be changed at runtime from /settings without restarting.

Default
15
Value
15
SPEEDTEST_ADMIN_EMAILVariable

First-run admin email (optional). When set together with SPEEDTEST_ADMIN_PASSWORD, the admin is auto-seeded on every boot (idempotent). Leave empty to use the /setup wizard instead.

SPEEDTEST_ADMIN_PASSWORDVariable

First-run admin password (optional, min 10 chars). Used together with SPEEDTEST_ADMIN_EMAIL. Updating this value rehashes the admin password on the next boot.

SPEEDTEST_PUBLIC_URLVariable

Public URL of the dashboard (e.g. https://speedtest.example.com). Used to build deep links in alert emails. Leave empty if unused.

SPEEDTEST_LOCALEVariable

BCP 47 locale for dates and times in the dashboard and alert emails (e.g. en-US, fr-FR, de-DE). Requires v1.13.0 or later.

Default
en-US
Value
en-US
SPEEDTEST_TIMEZONEVariable

IANA timezone for dates and times in the dashboard and alert emails (e.g. Europe/Paris, America/New_York). Requires v1.13.0 or later.

Default
UTC
Value
UTC
SPEEDTEST_TEST_DURATION_SVariable

Duration of each download/upload phase in seconds. Raise to 20-30 for multi-Gbps links so TCP slow-start does not dominate the sample.

Default
10
Value
10
SPEEDTEST_PARALLEL_STREAMSVariable

Parallel HTTP streams per phase. Increase (e.g. 16) on very high-bandwidth links to saturate the pipe.

Default
8
Value
8
SPEEDTEST_DB_PATHVariable

Absolute path to the SQLite database file. Must live under the App Data volume.

Default
/data/speedtest.db
Value
/data/speedtest.db
PORTVariable

Internal HTTP port. Only change if you know what you are doing - must match the WebUI port mapping above.

Default
3000
Value
3000
SPEEDTEST_WEBHOOK_URLVariable

Generic webhook URL for alerts (optional). Leave empty if unused.

SPEEDTEST_WEBHOOK_HEADERSVariable

Optional JSON object of extra headers for the generic webhook (e.g. {&quot;Authorization&quot;:&quot;Bearer xxx&quot;}).

SPEEDTEST_NTFY_URLVariable

Full ntfy URL including the topic, e.g. https://ntfy.sh/my-topic. Leave empty if unused.

SPEEDTEST_NTFY_TOKENVariable

Optional ntfy access token (for protected topics).

SPEEDTEST_DISCORD_WEBHOOKVariable

Discord webhook URL for alerts (optional).

SPEEDTEST_SLACK_WEBHOOKVariable

Slack incoming webhook URL for alerts (optional).

SPEEDTEST_SMTP_HOSTVariable

SMTP server hostname for email alerts (optional). Required together with SPEEDTEST_SMTP_FROM and SPEEDTEST_SMTP_TO to enable email.

SPEEDTEST_SMTP_PORTVariable

SMTP port. Defaults to 587 (STARTTLS).

Default
587
Value
587
SPEEDTEST_SMTP_SECUREVariable

'true' for implicit TLS (port 465), 'false' for STARTTLS, leave empty for auto-detect from port.

SPEEDTEST_SMTP_USERVariable

SMTP username (optional, for authenticated SMTP).

SPEEDTEST_SMTP_PASSVariable

SMTP password (optional, for authenticated SMTP).

SPEEDTEST_SMTP_FROMVariable

Sender address for alert emails, e.g. speedtest@example.com.

SPEEDTEST_SMTP_TOVariable

Recipient address for alert emails (single address or comma-separated list).

SPEEDTEST_OIDC_ISSUERVariable

OIDC issuer URL (optional), e.g. https://auth.example.com. Required together with the client id/secret to enable SSO.

SPEEDTEST_OIDC_CLIENT_IDVariable

OIDC client id (optional).

SPEEDTEST_OIDC_CLIENT_SECRETVariable

OIDC client secret (optional).

SPEEDTEST_OIDC_DISPLAY_NAMEVariable

Label shown on the OIDC sign-in button.

Default
SSO
Value
SSO
SPEEDTEST_OIDC_ADMIN_EMAILVariable

Email of the user to auto-promote to admin on first OIDC sign-in.

SPEEDTEST_OIDC_ALLOW_NEW_USERSVariable

'true' to auto-create accounts on first OIDC sign-in, 'false' to require admin pre-provisioning.

Default
true
Value
true