How to Deploy Speedtest Tracker: Automated Internet Performance Monitoring with Docker Compose

Speedtest Tracker is a self-hosted monitoring suite that continuously benchmarks your internet connection speed, ping, and packet loss using the official Ookla Speedtest CLI. Learn how to deploy Speedtest Tracker v1.15.1-ls180 with Docker Compose, configure automated cron schedules, secure the administrative panel, and integrate programmatic REST API tokens.

How to Deploy Speedtest Tracker: Automated Internet Performance Monitoring with Docker Compose

Speedtest Tracker is an open-source, self-hosted internet performance monitoring and SLA verification application. Built by Alex Justesen on the modern Laravel and Filament framework and packaged as a turnkey container by LinuxServer.io, Speedtest Tracker continuously benchmarks download speeds, upload bandwidth, latency, jitter, and packet loss using the official Ookla Speedtest CLI.

Speedtest Tracker Live Dashboard

Internet service providers frequently advertise nominal connection tiers (such as 500 Mbps down or 1 Gbps symmetric fiber), yet real-world speeds fluctuate dramatically due to neighborhood contention, peak-hour throttling, routing degradation, and bufferbloat. Sporadic manual speed tests conducted in a web browser fail to provide actionable historical evidence. Speedtest Tracker automates bandwidth testing on a predictable cron schedule, logging every execution to a persistent database and surfacing interactive visual telemetry through a public-facing dashboard and a password-protected administrative interface.

Upload, Ping, and Jitter Telemetry Charts

Core Architectural Features

  1. Automated & On-Demand Benchmarking: Executes scheduled network performance tests via standard cron expressions (SPEEDTEST_SCHEDULE) and supports instant, on-demand manual benchmarks triggered directly from the web interface or via authenticated REST API calls.
  2. Official Ookla Speedtest CLI Integration: Integrates the proprietary Ookla Speedtest CLI binary (/usr/bin/speedtest), capturing precise download and upload bandwidth (in bits and bytes), latency metrics (ping, low/high spread, jitter, interquartile mean [IQM]), and packet loss percentage against specific or automatically selected regional test servers.
  3. Reactive Filament v5 Administrative Portal: Features a clean, single-pane management dashboard powered by Livewire, providing detailed test records, server selector configuration, scheduled task intervals, data export capabilities (CSV/XLSX), and automated data pruning policies.
  4. Public Status Dashboard: Exposes a clean, read-only dashboard (PUBLIC_DASHBOARD=true) displaying current connection metrics, 24-hour historical line charts, and speed averages without requiring visitors or household members to authenticate.
  5. Multi-Channel Alerting & Webhooks: Supports proactive threshold notifications across platforms including Discord, Slack, Telegram, Gotify, Ntfy, Pushover, and custom HTTP webhooks when download or upload throughput dips below user-defined SLA limits.
  6. Programmatic REST API with Sanctum: Provides RESTful API endpoints (/api/v1/results, /api/v1/stats, /api/v1/speedtests/run) secured by Laravel Sanctum personal access tokens for seamless integration into Home Assistant, Grafana, or custom monitoring scripts.
  7. Zero Host Runtime Pollution: The entire application stack—PHP 8.x, Nginx, SQLite database engine, supervisord daemon, and Ookla CLI—runs encapsulated inside the Docker container. No local PHP, Composer, or system libraries are installed on the host.

---

Prerequisites

Before deploying Speedtest Tracker, verify that your host environment meets the following requirements:

  • Operating System: Linux (Ubuntu 22.04+, Debian 12+, Rocky Linux / RHEL 9+, Arch Linux), macOS, or Windows with WSL2.
  • Container Runtime: Docker Engine 24.0+ and Docker Compose v2 (docker compose version v2.24+).
  • Network Ports: Dedicated host port 3000 (default mapped to container HTTP port 80). If port 3000 is occupied, you can map any free host port (such as 3002, 3003, or 8080).
  • Network Connectivity: Unrestricted outbound TCP and UDP connectivity on ports 80, 443, and 8080 to Ookla Speedtest endpoints. Ensure firewall policies allow the container to reach external servers.
  • Storage & File Permissions: Host directory for persistent SQLite database storage and application configuration (./config). The LinuxServer container runs as an unprivileged user mapped to PUID=1000 and PGID=1000 (abc), requiring appropriate directory write access.
  • Host Tools: Standard system utilities (curl, openssl).

---

Exact Install Steps

Follow these sequential steps to configure, deploy, bootstrap, and verify Speedtest Tracker (lscr.io/linuxserver/speedtest-tracker:v1.15.1-ls180) using Docker Compose.

Step 1: Create Deployment Directory and Storage

Create an isolated directory on your host to house the deployment configuration and persistent application data:

mkdir -p speedtest-tracker/config
cd speedtest-tracker
chmod -R 777 ./config

Permission Note: LinuxServer.io images map internal container processes to user abc via PUID and PGID (default 1000:1000). Setting permissions on ./config ensures the container process can create and migrate the SQLite database file (/config/database/web.sqlite) upon first boot.

Step 2: Generate Encryption Key and Environment Configuration

Speedtest Tracker requires a 32-byte base64-encoded application key (APP_KEY) to encrypt session data and database secrets. Generate the key using openssl and write an environment file (.env):

# Generate 32-byte encryption key
APP_KEY="base64:$(openssl rand -base64 32)"

# Create environment configuration file
cat > .env <<EOF
COMPOSE_PROJECT_NAME=speedtest-tracker
SPEEDTEST_PORT=3000
SPEEDTEST_TAG=v1.15.1-ls180
APP_KEY=${APP_KEY}
APP_URL=http://localhost:3000
PUID=1000
PGID=1000
TZ=Etc/UTC
DB_CONNECTION=sqlite
DISPLAY_TIMEZONE=UTC
PUBLIC_DASHBOARD=true
SPEEDTEST_SCHEDULE=0 * * * *
ADMIN_NAME=Admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=ChangeThisPassword123!
EOF

chmod 600 .env

Key environment configuration parameters:

  • SPEEDTEST_PORT: The external host port exposed for web access (default 3000).
  • SPEEDTEST_TAG: Pinned container image release (v1.15.1-ls180).
  • APP_KEY: Application encryption key generated via OpenSSL.
  • PUBLIC_DASHBOARD: Enables read-only access to speed charts on the root URL (/) without requiring login.
  • SPEEDTEST_SCHEDULE: Standard 5-field cron expression for automated benchmarks (0 * * * * runs tests hourly at minute 0). Set to an empty string ("") if you only want manual testing.
  • ADMIN_EMAIL and ADMIN_PASSWORD: Credentials for the primary owner account created automatically during database migration.

Step 3: Write Docker Compose Configuration

Create compose.yml declaring the Speedtest Tracker service, persistent volume mount, environment variable pass-through, and container health checks:

cat > compose.yml <<'EOF'
services:
  speedtest-tracker:
    image: lscr.io/linuxserver/speedtest-tracker:${SPEEDTEST_TAG:-v1.15.1-ls180}
    container_name: ${COMPOSE_PROJECT_NAME}-server
    pull_policy: missing
    restart: unless-stopped
    ports:
      - "${SPEEDTEST_PORT:-3000}:80"
    environment:
      PUID: ${PUID:-1000}
      PGID: ${PGID:-1000}
      TZ: ${TZ:-Etc/UTC}
      APP_KEY: ${APP_KEY}
      APP_URL: ${APP_URL:-http://localhost:3000}
      DB_CONNECTION: sqlite
      DISPLAY_TIMEZONE: ${DISPLAY_TIMEZONE:-UTC}
      PUBLIC_DASHBOARD: ${PUBLIC_DASHBOARD:-true}
      SPEEDTEST_SCHEDULE: ${SPEEDTEST_SCHEDULE:-0 * * * *}
      ADMIN_NAME: ${ADMIN_NAME:-Admin}
      ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@example.com}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
    volumes:
      - ./config:/config
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost/api/healthcheck || exit 1"]
      interval: 5s
      timeout: 3s
      retries: 40
      start_period: 10s
networks:
  default:
    name: ${COMPOSE_PROJECT_NAME}-net
EOF

Step 4: Launch the Container

Start the service in detached mode using Docker Compose:

docker compose up -d

Monitor the internal startup sequence and await successful container initialization:

# Poll healthcheck endpoint until service reports ready
until curl -fsS http://localhost:3000/api/healthcheck 2>/dev/null | grep -q "running"; do
  echo "Waiting for Speedtest Tracker database migrations to complete..."
  sleep 2
done

echo "Speedtest Tracker is initialized and running!"

Expected response from /api/healthcheck:

{"message":"Speedtest Tracker is running!"}

Step 5: Verify Operator CLI Access

Administrative and diagnostic commands can be executed inside the running container using Docker's native execution tools without installing PHP or artisan on the host:

# Helper function executing artisan commands as user abc
artisan() {
  docker compose exec -u abc speedtest-tracker php /app/www/artisan "$@"
}

# Inspect application framework and runtime versions
artisan about --only=environment

# Check database migration status
artisan migrate:status | tail -n 5

The output confirms Laravel 13.x and PHP 8.x operating under production settings with all database migrations executed.

---

Browser Setup

Once the container reports healthy, navigate to the web application in your browser.

About these screenshots: the charts show 48 seeded demo results ("Demo Server", "Demo ISP"), not real speed tests, so no real IP or ISP is shown. A fresh install starts empty and fills in as SPEEDTEST_SCHEDULE runs real tests.

1. Accessing the Interface

  • Public Dashboard (http://localhost:3000/): Displays real-time download/upload speed gauges, latency, packet loss, and 24-hour trend lines once initial tests are completed.
  • Admin Control Panel (http://localhost:3000/admin): Automatically redirects to the secure Filament login page (http://localhost:3000/admin/login).

Filament Sign-In Interface

2. Administrator Sign-In

Log in using the credentials defined in your .env configuration:

  • Email: admin@example.com (or your configured ADMIN_EMAIL)
  • Password: The plaintext password assigned to ADMIN_PASSWORD

Admin Dashboard Overview

Upon successful sign-in, the administrative dashboard presents summary statistics:

  • Total Tests Completed: Total historical tests recorded in the database.
  • Latest Speed Metrics: Download throughput, upload throughput, ping, and server identification.
  • Quick Benchmark Trigger: A topbar rocket action icon (Speedtest) that allows initiating an ad-hoc Ookla benchmark on demand.

3. Reviewing Historical Results

Select Results from the left navigation sidebar to view the granular historical audit log:

Historical Speedtest Results Table

The Results view provides:

  • Exact benchmark timestamps and status badges (Completed, Failed).
  • Testing target server identification (name, location, server ID).
  • Numerical throughput metrics (Mbps) with latency, jitter, and packet loss percentages.
  • Table export options allowing one-click export to CSV or Excel for SLA dispute documentation with your internet service provider.

4. Configuring Settings & Schedules

Access the Settings menu to customize application behavior:

  • General Settings: Set the application timezone and site branding.
  • Speedtest Settings: Customize the cron expression (SPEEDTEST_SCHEDULE), select a pinned Ookla Speedtest server ID to test against a constant network peer, or allow automatic low-latency server selection.
  • Thresholds & Alerts: Define minimum acceptable download (e.g. 300 Mbps) or upload thresholds to trigger alerts when your connection underperforms.
  • Notification Integrations: Add webhook URLs or bot tokens for Discord, Slack, Telegram, Pushover, or Gotify.

---

Verification

Validate the operational readiness, container health, and programmatic API access using the following terminal commands.

1. Check Container Health Status

Verify that Docker Compose reports the container as healthy:

docker compose ps

Expected output:

NAME                     IMAGE                                         COMMAND   SERVICE             CREATED          STATUS                    PORTS
speedtest-tracker-server  lscr.io/linuxserver/speedtest-tracker:v1.15.1-ls180 "/init"   speedtest-tracker   45 seconds ago   Up 45 seconds (healthy)   0.0.0.0:3000->80/tcp

2. Verify REST API Authentication & Endpoints

Speedtest Tracker provides an authenticated REST API. Generate a personal access token via PHP inside the container:

# Generate a Sanctum personal access token
API_TOKEN=$(docker compose exec -u abc speedtest-tracker php -r "
  require '/app/www/vendor/autoload.php';
  \$app = require '/app/www/bootstrap/app.php';
  \$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
  echo App\Models\User::first()->createToken('api-monitor', ['results:read', 'speedtests:run'])->plainTextToken;
")

echo "Generated Token: ${API_TOKEN:0:8}..."

Verify that unauthenticated requests are rejected with HTTP 401:

curl -sS -o /dev/null -w "Unauthenticated request status: %{http_code}\n" \
  -H "Accept: application/json" \
  http://localhost:3000/api/v1/results

Expected output:

Unauthenticated request status: 401

Query the latest benchmark result using the generated Bearer token:

curl -sS -H "Authorization: Bearer $API_TOKEN" \
  -H "Accept: application/json" \
  http://localhost:3000/api/v1/results/latest

Query aggregated 24-hour performance statistics:

curl -sS -H "Authorization: Bearer $API_TOKEN" \
  -H "Accept: application/json" \
  http://localhost:3000/api/v1/stats

Expected JSON response:

{
  "message": "ok",
  "data": {
    "total_results": 48,
    "download": {
      "avg": 430770000,
      "avg_bits_human": "430.77 Mbps"
    },
    "upload": {
      "avg": 96180000,
      "avg_bits_human": "96.18 Mbps"
    },
    "ping": {
      "avg": 16.25
    }
  }
}

3. Verify Public Dashboard Status

Ensure the public dashboard serves HTTP 200 without redirecting:

curl -sS -o /dev/null -w "Public dashboard response: %{http_code}\n" http://localhost:3000/

Expected output:

Public dashboard response: 200

---

Troubleshooting

The table below outlines common deployment challenges, diagnostic indicators, and verified solutions.

Issue / SymptomPossible CauseResolution
First visit to / redirects to /getting-started with HTTP 302Fresh installation with zero recorded speedtest resultsRun an initial benchmark by clicking Speedtest in the admin UI or trigger POST /api/v1/speedtests/run. Once at least one test is recorded, / renders the dashboard.
Permission denied writing SQLite database (open /config/database/web.sqlite: permission denied)Persistent host directory ./config is not writable by container user abc (UID 1000, GID 1000)Execute chmod -R 777 ./config or chown -R 1000:1000 ./config on the host, then run docker compose restart.
Port collision error (bind: address already in use on host port 3000)Another service or container is already listening on host TCP port 3000Update SPEEDTEST_PORT in .env to an alternate unused host port (such as 3002, 3003, or 8080) and restart with docker compose up -d.
Healthcheck fails (curl -fsS http://localhost/api/healthcheck returns error)Container is still executing initial SQLite database migrations and asset cachingInspect container startup logs using docker compose logs -f speedtest-tracker. Allow up to 30 seconds on lower-power CPUs for migration completion.
Admin login fails with invalid credentialsPassword was changed or .env ADMIN_PASSWORD was not picked up due to existing databaseReset the admin password directly using the container PHP CLI: docker compose exec -u abc speedtest-tracker php -r "require '/app/www/vendor/autoload.php'; \$app = require '/app/www/bootstrap/app.php'; \$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); \$u = App\Models\User::first(); \$u->password = Hash::make('NewPassword123!'); \$u->save(); echo 'Password updated\n';"
Automated tests fail to run on scheduleSPEEDTEST_SCHEDULE is blank, formatted incorrectly, or cron daemon failed inside containerVerify SPEEDTEST_SCHEDULE syntax in .env (e.g. 0 * * * * for hourly runs). Inspect cron logs with docker compose exec speedtest-tracker crontab -l and ensure the container's timezone matches expectations.
Speedtest CLI error (Server returned error: 500 or timeout)Outbound network firewall is blocking Ookla Speedtest TCP/UDP ports or ISP DNS is timing outVerify that the container can resolve and reach Ookla servers: docker compose exec speedtest-tracker curl -I https://www.speedtest.net. If using custom DNS (such as Pi-hole or AdGuard Home), verify Ookla endpoints are not blocked.
App key error (No application encryption key has been specified)The APP_KEY environment variable is unset, empty, or missing the base64: prefixGenerate a fresh key with echo "base64:$(openssl rand -base64 32)", set APP_KEY in .env, and recreate the container with docker compose up -d.

---

Assumptions & Decisions

  1. Pinned Release Tag: Standardized on lscr.io/linuxserver/speedtest-tracker:v1.15.1-ls180 rather than floating latest. This ensures immutable deployment reproducibility, preventing breaking changes in database schema, Laravel dependencies, or Filament components.
  2. First-Run Owner Bootstrap: The deployment takes advantage of LinuxServer's upstream environment variables ADMIN_EMAIL and ADMIN_PASSWORD. When the container runs its initial migration (artisan migrate), the administrative account is created automatically, eliminating manual initial registration screens.
  3. Storage & Engine Architecture: Utilizes SQLite stored in ./config/database/web.sqlite. For single-node homelab internet monitoring, SQLite provides exceptional read performance, minimal RAM overhead (~100 MB total container memory), and zero database server management.
  4. Synthetic Lab Benchmarking vs. Real Runs: During automated lab validation, 48 synthetic results across a 24-hour curve were seeded with documentation-reserved IP addresses (192.0.2.x) and generic ISP tags ("Demo ISP"). This verified line chart rendering, averages, and API stats without transmitting real physical location data, ISP details, or public IP addresses onto public screenshots. Production deployments perform real Ookla benchmarks via SPEEDTEST_SCHEDULE.
  5. Production Image Execution Constraints: The official production container image removes development tooling such as laravel/tinker and fakerphp/faker to maintain a minimal container footprint. Administrative automation and token generation are executed using targeted PHP bootstrap scripts run under the unprivileged abc user.
  6. Zero Host Pollution: All operations, queue workers, web servers, and Ookla CLI tests execute exclusively inside Docker containers, keeping the host operating system completely clean.