Skip to content

rawsocket-dev/myspeed

Repository files navigation

speedtest

Self-hosted internet speed test, distributed as a single ~25 MB Docker image. Browser-side measurements use the open-source @cloudflare/speedtest library against an HTTP server in this repo. History is persisted to SQLite with shareable per-test URLs (UUIDv7).

Measures download throughput, upload throughput, idle latency, jitter, and loaded latency.

Quick start

The image is published to the Forgejo container registry at git.fiber.house/seitz/myspeed. If the package is private, log in once with a Forgejo access token (Settings → Applications → Generate New Token, scope: read:package):

echo $FORGEJO_TOKEN | docker login git.fiber.house -u seitz --password-stdin

Then with compose (recommended — uses a named Docker volume so the distroless nonroot user can write without any chown ceremony):

docker compose up -d

Or with docker run and a named volume:

docker run -d --restart unless-stopped \
  -p 8080:8080 \
  -v speedtest-data:/data \
  git.fiber.house/seitz/myspeed:latest

Open http://localhost:8080.

Want a bind mount instead? Use -v $PWD/data:/data and pre-chown the host directory: mkdir -p data && sudo chown 65532:65532 data. The image runs as UID 65532 (distroless nonroot) and a host-owned bind mount inherits the host's permissions, so the container can't write to it without that one-time chown.

Configuration

All config is via environment variables; defaults are sane for a homelab.

Var Default Purpose
SPEEDTEST_LISTEN :8080 Bind address
SPEEDTEST_DB_PATH /data/speedtest.db SQLite file path
SPEEDTEST_MAX_DOWN_BYTES 1073741824 Per-request download cap (1 GiB)
SPEEDTEST_MAX_UP_BYTES 1073741824 Per-request upload cap (1 GiB)
SPEEDTEST_IGNORE_FORWARDED false Set true only when binary is directly internet-exposed
SPEEDTEST_ADMIN_TOKEN (unset) If set, requests with Authorization: Bearer <token> see client_ip in API responses
SPEEDTEST_LOG_LEVEL info debug/info/warn/error

Deployment notes

Behind a reverse proxy (Caddy/Traefik/nginx in homelab): default config is correct. The leftmost X-Forwarded-For value becomes the recorded client IP.

Direct internet exposure (no proxy): set SPEEDTEST_IGNORE_FORWARDED=true so callers can't spoof their IP via X-Forwarded-For.

Privacy: API responses omit client_ip by default. Only requests bearing the admin bearer token see IPs. This protects visitor IPs from casually leaking through shareable result URLs.

Endpoints

  • GET / — SPA
  • GET /__down?bytes=N — streams N zero bytes
  • POST /__up — accepts and discards body
  • GET /__ping — 204 with Server-Timing: cfRequestDuration;dur=0
  • GET /__meta — JSON {clientIp, hostname, version} for the SPA
  • GET /__tracetext/plain cf-trace format for @cloudflare/speedtest
  • GET /healthz — DB-aware liveness
  • POST /api/results — submit a finished test summary
  • GET /api/results?limit=N&since=RFC3339 — list, newest first
  • GET /api/results/:public_id — single test
  • DELETE /api/results/:public_id — delete
  • GET /api/results.csv — CSV export

Local development

# Frontend dev server (proxies to backend on :8080):
cd web && pnpm install && pnpm dev

# In a second terminal:
cd web && pnpm build && cp -r dist ../internal/server/dist
SPEEDTEST_DB_PATH=/tmp/speedtest-dev.db go run ./cmd/speedtest

Or just rebuild the Docker image: docker compose up -d --build.

Architecture

See docs/superpowers/specs/2026-05-01-speedtest-clone-design.md.

License

MIT — see LICENSE.

This project would not exist without @cloudflare/speedtest (MIT, © Cloudflare), which provides the in-browser measurement engine. We use it as a library dependency; we do not include any Cloudflare branding.

About

Self-hosted internet speed test — Go backend with embedded web UI, history, ASN/ISP enrichment, distroless image.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors