A self-hosted, internet speed test tracking application.
1.1K
A self-hosted, full-stack internet speed test tracking application with automated scheduling, role-based authentication, multi-database support, threshold alerting, and a modern dark/light UI.
PulseNet is a self-hosted SpeedTest Tracker that runs Ookla-powered speed tests on demand or on a configurable schedule, stores every result in a database, and presents everything through a responsive React dashboard. It is built to run entirely in Docker with a single command and supports SQLite, PostgreSQL, and MySQL/MariaDB out of the box.
admin and user with a clearly defined permission boundary.env on first run.env — no email server needed; set RESET_PASSWORD_EMAIL + RESET_PASSWORD_NEW, restart, donelocalStorage| Layer | Technology | Version |
|---|---|---|
| Backend | Python, FastAPI | 3.11 / 0.115 |
| Speed tests | speedtest-cli (Ookla) | 2.1.3 |
| ORM | SQLAlchemy | 2.0 |
| Scheduler | APScheduler | 3.10 |
| Auth | python-jose (JWT) + bcrypt | 3.3 / 4.0 |
| HTTP client | httpx (for alerts) | 0.27 |
| Frontend | React + Vite | 18 / 5 |
| Charts | Recharts | 2.13 |
| Routing | react-router-dom | 6 |
| Date handling | date-fns | 4 |
| Export | xlsx (SheetJS), jsPDF | — |
| Web server | Nginx | 1.27 |
| Database | SQLite / PostgreSQL 16 / MariaDB 11 | — |
| Container | Docker + Docker Compose | — |
speedtest-tracker/
├── backend/
│ ├── main.py # FastAPI app — routes, models, scheduler, alerts, auth
│ ├── requirements.txt # Python dependencies
│ ├── .env # Environment config (credentials, DB URL, secret key)
│ └── Dockerfile
│
├── frontend/
│ ├── src/
│ │ ├── api/
│ │ │ └── index.js # API client with JWT injection and 401 handling
│ │ ├── contexts/
│ │ │ ├── AuthContext.jsx # Auth state, login/logout, user object
│ │ │ └── ThemeContext.jsx # Dark/light theme state and localStorage sync
│ │ ├── components/
│ │ │ ├── Sidebar.jsx # Collapsible nav with theme toggle and logout
│ │ │ ├── SpeedGauge.jsx # Animated SVG arc gauge
│ │ │ ├── ResultsChart.jsx # Time-range chart with smart axis labels
│ │ │ ├── ResultsTable.jsx # Results table with ISP column and detail modal
│ │ │ ├── ResultDetail.jsx # Per-result modal with full metadata
│ │ │ ├── ExportMenu.jsx # CSV / XLSX / PDF export dropdown
│ │ │ ├── ProtectedRoute.jsx
│ │ │ └── StatCard.jsx
│ │ ├── pages/
│ │ │ ├── Login.jsx # Split-screen login with theme toggle
│ │ │ ├── Dashboard.jsx # Gauges, session stats, speed history chart
│ │ │ ├── History.jsx # Full chart + paginated results table + export
│ │ │ ├── Schedule.jsx # Auto-schedule config with live countdown
│ │ │ └── Settings.jsx # Profile, backup/restore, alerts, user management
│ │ ├── App.jsx # Router, sidebar state, auth guard
│ │ ├── main.jsx
│ │ └── index.css # CSS variables, dark + light themes, global styles
│ ├── nginx.conf # Nginx SPA config + /api reverse proxy
│ ├── Dockerfile # Multi-stage build (Node build → Nginx serve)
│ ├── index.html
│ └── package.json
│
├── docker-compose.yml # All services + optional DB profiles
└── README.md
That is all. No Node.js, Python, or database installation needed for the Docker path.
# 1. Clone or extract the project
cd speedtest-tracker
# 2. (Optional) Edit credentials before first run
nano backend/.env
# 3. Build and start
docker compose up --build -d
# 4. Open in browser
open http://localhost:3001
# 5. View live logs
docker compose logs -f
The application will be available at http://localhost:3001.
Default login credentials (set in backend/.env):
| Field | Default value |
|---|---|
| Email or username | [email protected] or admin |
| Password | Admin123! |
Important: Change the
ADMIN_PASSWORDandSECRET_KEYinbackend/.envbefore any production or internet-facing deployment.
PulseNet supports three database engines. Select one by setting DATABASE_URL in backend/.env and using the matching Docker Compose profile.
# backend/.env
DATABASE_URL=sqlite:////data/speedtest.db
# Start
docker compose up --build -d
Best for: single-user, home lab, or evaluation. Data is stored in a named Docker volume.
# backend/.env
DATABASE_URL=postgresql://pulsenet:pulsenet_secret@postgres:5432/pulsenet
# Start with the postgres profile
docker compose --profile postgres up --build -d
# backend/.env
DATABASE_URL=mysql+pymysql://pulsenet:pulsenet_secret@mysql:3306/pulsenet
# Start with the mysql profile
docker compose --profile mysql up --build -d
| SQLite | PostgreSQL | MySQL/MariaDB | |
|---|---|---|---|
| Setup required | None | Automatic | Automatic |
| Production ready | ⚠️ Limited | ✅ Yes | ✅ Yes |
| Recommended for | Dev / home lab | Production | Production |
| Docker profile | (default) | postgres | mysql |
Backend
cd backend
# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Copy and edit the env file
cp .env .env.local
# Run the development server
uvicorn main:app --reload --port 8000
The API will be available at http://localhost:8000. Interactive docs at http://localhost:8000/docs.
Frontend
cd frontend
npm install
npm run dev # Starts on http://localhost:3000
# /api requests are proxied to http://localhost:8000
All configuration lives in backend/.env. The file is read at container startup.
# ── Database ─────────────────────────────────────────────────────
DATABASE_URL=sqlite:////data/speedtest.db
# DATABASE_URL=postgresql://pulsenet:secret@postgres:5432/pulsenet
# DATABASE_URL=mysql+pymysql://pulsenet:secret@mysql:3306/pulsenet
# ── Security ─────────────────────────────────────────────────────
# Generate a strong key with: openssl rand -hex 32
SECRET_KEY=replace-this-with-a-random-64-character-string
ACCESS_TOKEN_EXPIRE_MINUTES=1440 # 24 hours
# ── Bootstrap Admin ──────────────────────────────────────────────
# Applied (or re-applied) on every container restart.
ADMIN_USERNAME=admin
[email protected]
ADMIN_PASSWORD=Admin123!
# ── Password Reset ───────────────────────────────────────────────
# Uncomment both lines, restart the backend, then comment out again.
# [email protected]
# RESET_PASSWORD_NEW=NewStrongPassword456!
backend/.env — update ADMIN_PASSWORD to the new value.docker compose restart backendbackend/.env, uncomment and fill in:
[email protected]
RESET_PASSWORD_NEW=TheirNewPassword123!
docker compose restart backendopenssl rand -hex 32
Paste the output as the value of SECRET_KEY. Never commit this value to version control.
Authentication uses JWT bearer tokens. Tokens are issued on login and expire after ACCESS_TOKEN_EXPIRE_MINUTES (default 24 hours). Tokens are stored in localStorage and automatically attached to every API request.
New user accounts can only be created by an admin through Settings → Users.
| Action | User | Admin |
|---|---|---|
| Sign in | ✅ | ✅ |
| Run a manual speed test | ✅ | ✅ |
| View all test results | ✅ | ✅ |
| View individual result details | ✅ | ✅ |
| Export results (CSV / XLSX / PDF) | ✅ | ✅ |
| Change own avatar, username, email | ✅ | ✅ |
| Change own password | ✅ | ✅ |
| Configure auto-schedule | ❌ | ✅ |
| Delete individual results | ❌ | ✅ |
| Delete all results | ❌ | ✅ |
| Download backup | ❌ | ✅ |
| Restore from backup | ❌ | ✅ |
| Configure alert thresholds | ❌ | ✅ |
| Configure notification channels | ❌ | ✅ |
| Test alert channels | ❌ | ✅ |
| Create new user accounts | ❌ | ✅ |
| Change user roles | ❌ | ✅ |
| Deactivate user accounts | ❌ | ✅ |
A split-screen page with an animated speed-pulse graphic on the left and the sign-in form on the right. On mobile the branding panel collapses to a compact logo. A dark/light theme toggle is available in the top-right corner before authentication.
The "Create Account" tab explains that new accounts are created by an admin — it does not expose a public registration form.
The main view after login. Contains:
Four equal-height metric cards across the top:
Speed History chart — a time-series line chart showing download (cyan), upload (orange), and ping (green dashed) with:
HH:mm:ss, HH:mm, Mon HH:mm, or Jan 5 depending on the visible spanLatest test info bar — server name, ISP, and IP address from the most recent result with a "View details" button
Full test history with:
Two-column layout:
The schedule configuration persists to the database and is restored automatically on container restart.
A tabbed settings page. Tabs visible to all users:
Profile — upload an avatar image, change display name and email, change password with a real-time strength meter (5 checks: length, uppercase, lowercase, number, special character).
Additional tabs visible to admins only:
Backup & Restore — download all speed test results as a JSON file, or upload a previously exported file to restore records. Existing records (matched by ID) are skipped; only new records are inserted. The backup format is documented in the Backup & Restore section.
Alerts — configure speed thresholds and notification channels (see Alert Channels).
Users — a table of all registered accounts with inline role selector and deactivate action. A "New user" form lets admins create accounts for others.
Alerts are checked after every speed test (manual or scheduled). An alert fires when any of the configured thresholds are breached, subject to the cooldown period.
Channels are arranged in a 2 × 2 grid in Settings → Alerts:
| Channel | What you need |
|---|---|
| Discord | A webhook URL from Server Settings → Integrations → Webhooks |
| Telegram | A bot token from @BotFather and a chat/group ID from @userinfobot |
| Email (SMTP) | SMTP host, port, username, password, and a recipient address |
| Generic Webhook | Any URL that accepts POST (or PUT) with a JSON body |
| Field | Description |
|---|---|
| Min download (Mbps) | Alert if download falls below this value |
| Min upload (Mbps) | Alert if upload falls below this value |
| Max ping (ms) | Alert if ping exceeds this value |
| Cooldown (minutes) | Minimum gap between consecutive alerts (default 30) |
Each channel has a Send test button that fires an immediate test notification without needing a threshold breach.
🚨 PulseNet Speed Alert
• Download 3.2 Mbps < threshold 10 Mbps
• Ping 187.4 ms > threshold 100 ms
Test recorded at 2026-04-27T08:14:00Z
Backups are JSON files with the following structure:
{
"version": "2.0",
"app": "pulsenet",
"exported_at": "2026-04-27T08:00:00Z",
"total": 142,
"results": [
{
"id": 1,
"timestamp": "2026-04-20T10:00:00Z",
"download_mbps": 47.23,
"upload_mbps": 18.91,
"ping_ms": 22.5,
"server_name": "Nairobi IXP",
"server_sponsor": "Safaricom",
"server_location": "Nairobi, Kenya",
"server_country": "Kenya",
"isp": "Safaricom",
"ip_address": "105.163.x.x",
"triggered_by": "scheduled"
}
]
}
User accounts and passwords are not included in backups. The backup file is safe to share for data migration purposes.
id already exists in the target database are skipped (no overwrite).Restored 98 records · Skipped 44.All endpoints are prefixed with /api. Authenticated endpoints require a Bearer token in the Authorization header.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST | /api/auth/login | None | Sign in; returns JWT + user object |
GET | /api/auth/me | User | Get current user profile |
PUT | /api/auth/me | User | Update username, email, or avatar |
POST | /api/auth/change-password | User | Change own password |
POST | /api/auth/register | Admin | Create a new user account |
| Method | Endpoint | Description |
|---|---|---|
GET | /api/users | List all users |
PUT | /api/users/{id}/role | Change a user's role |
DELETE | /api/users/{id} | Deactivate a user |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST | /api/speedtest/run | User | Trigger a manual speed test |
GET | /api/speedtest/status | User | Running state + next scheduled run |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /api/results | User | List results (?skip=0&limit=200) |
GET | /api/results/latest | User | Most recent result |
GET | /api/results/{id} | User | Single result by ID |
DELETE | /api/results/{id} | Admin | Delete one result |
DELETE | /api/results | Admin | Delete all results |
GET | /api/stats | User | Aggregated statistics |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /api/schedule | User | Get current schedule config |
POST | /api/schedule | Admin | Update schedule config |
Schedule payload: { "enabled": true, "interval_minutes": 60 }
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /api/alert-config | Admin | Get alert configuration |
PUT | /api/alert-config | Admin | Save alert configuration |
POST | /api/alert-config/test | Admin | Send test notification |
Test payload: { "channel": "discord" } — valid channels: discord, telegram, email, webhook.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /api/backup | Admin | Download full JSON backup |
POST | /api/restore | Admin | Upload and restore a backup file (multipart/form-data) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /api/health | None | Health check; returns { "status": "ok", "version": "2.0.0" } |
Interactive API documentation is available at http://localhost:8000/docs when the backend is running.
| Container | Name |
|---|---|
| Backend API | pulsenet-backend-v3 |
| Frontend (Nginx) | pulsenet-frontend-v3 |
| PostgreSQL | pulsenet-postgres-v3 |
| MySQL/MariaDB | pulsenet-mysql-v3 |
| Service | Host port | Container port |
|---|---|---|
| Frontend | 3001 | 80 |
| Backend API | 8000 | 8000 |
# Start (SQLite default)
docker compose up --build -d
# Start with PostgreSQL
docker compose --profile postgres up --build -d
# Start with MySQL / MariaDB
docker compose --profile mysql up --build -d
# View logs
docker compose logs -f
docker compose logs -f backend
# Restart backend only (e.g. after .env change)
docker compose restart backend
# Stop all services
docker compose down
# Stop and remove all data volumes (full reset)
docker compose down -v
# Rebuild after code changes
docker compose up --build -d
| Volume | Contents |
|---|---|
speedtest-data | SQLite database file |
postgres-data | PostgreSQL data directory |
mysql-data | MySQL/MariaDB data directory |
Data persists across docker compose down restarts. Use docker compose down -v only when you want a complete wipe.
The following features are planned for upcoming phases:
.env editingThis project is for personal and internal use. No licence has been applied at this time.
Built with ⚡ by the PulseNet team — a self-hosted SpeedTest Tracker that respects your data.
Content type
Image
Digest
sha256:9126c128a…
Size
81.8 MB
Last updated
about 2 months ago
docker pull oste/pulsenet:backend-latest