Speedtest Tracker mit Docker Compose: Internetanschluss automatisch messen
Speedtest Tracker misst den Internetanschluss per Zeitplan mit der Ookla-CLI und speichert die Ergebnisse in SQLite. Die Anleitung zeigt Einrichtung mit Docker Compose (getestet mit v1.15.0), Healthcheck, Backup, Updates und typische Fehler.
Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Eine einzelne Browsermessung klärt nicht, ob Anschluss, WLAN oder Tageszeit für ein zähes Internet verantwortlich sind. Speedtest Tracker misst nach Zeitplan, speichert Ping, Download, Upload und Messserver je Ergebnis und zeigt den Verlauf als Diagramm.
Nutzen und Grenzen
Speedtest Tracker (Laravel, MIT-Lizenz) ruft die Ookla-Speedtest-CLI auf und speichert die Ergebnisse. Die Anleitung beschreibt v1.15.0 vom 21.08.2026.
Wofür sich das Werkzeug eignet:
- Langzeitverlauf der Anschlussleistung, etwa Einbrüche zu bestimmten Uhrzeiten.
- Gesprächsgrundlage gegenüber Provider oder Dienstleister.
- Benachrichtigung, wenn Download, Upload oder Ping unter selbst gesetzte Schwellwerte fallen.
- Auswertung per API, Prometheus oder InfluxDB, falls bereits ein Monitoring vorhanden ist.
Wichtige Abgrenzung: Die Ergebnisse sind kein amtlicher Nachweis. Für Festnetzanschlüsse bietet die Bundesnetzagentur mit der Desktop-App der Breitbandmessung ein verbindliches Verfahren (30 Messungen an drei Kalendertagen innerhalb von 14 Tagen). Erst dieses Protokoll begründet Minderungs- oder Sonderkündigungsrechte, Speedtest Tracker ist ein Frühwarnsystem.
Weitere Grenzen:
- Gemessen wird vom Docker-Host aus: WLAN oder ein langsamer Switch-Port verfälschen das Ergebnis.
- Jede Messung belastet den Anschluss, bei Volumentarifen oder Mobilfunk-Backup nutzen Sie einen sparsamen Zeitplan.
- Gemessen wird gegen Ookla-Server, die CLI läuft mit
--accept-license --accept-gdpr: Ookla-Bedingungen gelten. - Unterstützt sind laut Doku Docker, Compose, Kubernetes, QNAP, Synology und Unraid, nicht Bare-Metal oder Proxmox-LXC ohne Docker.
Voraussetzungen und Ressourcen
- Linux-Host (x86-64 oder ARM64) mit Docker und Compose-Plugin: 1 CPU-Kern, 512 MB RAM, 1 GB Speicher.
- Kabelanbindung an den Router, damit nicht das WLAN gemessen wird.
- Ein freier Port für die Weboberfläche, in dieser Anleitung
127.0.0.1:18472. - Ausgehender Zugriff auf
icanhazip.comund Ookla-Server. DNS-Blocklisten, dieicanhazip.comsperren, lassen Messungen fehlschlagen. opensslauf dem Host, um den Anwendungsschlüssel zu erzeugen.
Im Test: 152 MiB RAM im Leerlauf, Image 376 MB, SQLite wächst um wenige KB je Messung.
Plattformen, Image und getestete Version
Das Image stammt von LinuxServer.io (lscr.io/linuxserver/speedtest-tracker), Architekturen:
| Architektur | Tag | Typische Hosts |
|---|---|---|
| x86-64 | amd64-latest | Server, Mini-PCs, die meisten NAS mit Intel oder AMD |
| arm64 | arm64v8-latest | Raspberry Pi 4 und 5 mit 64-Bit-System, ARM-Server |
Als Beispiel dient amd64 mit dem Tag v1.15.0-ls172: php artisan about meldet Speedtest Tracker v1.15.0, Laravel 13.26.0, PHP 8.5.10 und Filament v5.7.6, Speedtest by Ookla 1.2.0.84. Die Doku zeigt latest, nutzen Sie einen festen Tag. Ein neuerer Build derselben Version trägt den Tag v1.15.0-ls173.
Vollständige compose.yaml und .env
Für einen Standort reicht SQLite (alternativ MariaDB, MySQL, Postgres). Legen Sie ein Projektverzeichnis an, darin die compose.yaml:
name: speedtest
services:
speedtest-tracker:
image: lscr.io/linuxserver/speedtest-tracker:v1.15.0-ls172
container_name: speedtest-tracker
restart: unless-stopped
ports:
- "127.0.0.1:18472:80"
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=Europe/Berlin
- APP_KEY=${APP_KEY}
- APP_URL=${APP_URL}
- DB_CONNECTION=sqlite
- SPEEDTEST_SCHEDULE=${SPEEDTEST_SCHEDULE}
- DISPLAY_TIMEZONE=Europe/Berlin
- PRUNE_RESULTS_OLDER_THAN=365
volumes:
- ./data:/config
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost/api/healthcheck || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
Die Werte stehen in der .env (Beispiel ohne echten Schlüssel):
# Benutzer und Gruppe des Hosts, ermitteln mit: id
PUID=1000
PGID=1000
# Erzeugen mit: echo "base64:$(openssl rand -base64 32)"
APP_KEY=base64:HIER_EIGENEN_SCHLUESSEL_EINTRAGEN
# Adresse, unter der die Oberfläche erreichbar ist
APP_URL=http://127.0.0.1:18472
# Minute 17 alle sechs Stunden
SPEEDTEST_SCHEDULE=17 */6 * * *
Schlüssel erzeugen, Ausgabe samt Präfix base64: übernehmen:
$ echo "base64:$(openssl rand -base64 32 2>/dev/null)"
$ chmod 600 .env
docker compose config läuft ohne Fehler durch und zeigt APP_KEY: base64:… mit Ihrem Schlüssel sowie den Zeitplan.
Parameter und Dateipfade
| Variable | Pflicht | Wirkung |
|---|---|---|
PUID, PGID | ja | Benutzer und Gruppe, unter denen die Anwendung läuft. Im Test gehörten dadurch alle Dateien in ./data dem Host-Benutzer mit UID 1000. |
APP_KEY | ja | Schlüssel zum Ver- und Entschlüsseln von Sitzungen und sensiblen Daten. Ohne ihn bricht die Initialisierung ab. |
APP_URL | ja | Adresse für Links in Mails und Benachrichtigungen. Hinter einem Proxy die öffentliche HTTPS-Adresse. |
DB_CONNECTION | nein | sqlite, alternativ mariadb, mysql oder pgsql mit den zugehörigen DB_*-Variablen. |
SPEEDTEST_SCHEDULE | nein | Cron-Ausdruck für automatische Messungen. Ohne Wert misst die Anwendung nur auf Knopfdruck. |
SPEEDTEST_SERVERS | nein | Kommagetrennte Ookla-Server-IDs, aus denen zufällig gewählt wird. |
PRUNE_RESULTS_OLDER_THAN | nein | Ergebnisse älter als die angegebene Zahl an Tagen werden gelöscht. 0 behält alles. |
DISPLAY_TIMEZONE | nein | Zeitzone für die Anzeige. Die Anwendung selbst rechnet intern in UTC. |
ADMIN_EMAIL, ADMIN_PASSWORD | nein | Zugangsdaten des ersten Admins, wirken laut Dokumentation nur bei der Ersteinrichtung. |
ASSET_URL | nein | Adresse für CSS und JavaScript, hinter einem Reverse Proxy zusammen mit APP_URL setzen. |
Alles Persistente liegt in ./data (im Container /config): database.sqlite, keys/ mit selbstsigniertem Zertifikat, log/, nginx/, php/. Eigene Zertifikate: cert.crt (volle Kette) und cert.key in /config/keys.
Installation und Start
# Image laden$ docker compose pull# Container im Hintergrund starten$ docker compose up -d# Status und Healthcheck beobachten$ docker compose ps
Im Test lieferte der Healthcheck nach 52 Sekunden HTTP 200, danach (healthy). Version im Log:
$ docker compose logs --no-log-prefix | grep -i version# Ausgabe im Test:
# Linuxserver.io version: v1.15.0-ls172
Programmversion im Container:
$ docker compose exec speedtest-tracker bash -c 'cd /app/www && php artisan about'
docker compose ps zeigt nach etwa einer Minute (healthy), das Log nennt v1.15.0-ls172.
Erstkonfiguration
Anmeldung: /admin/login. Laut Doku (im Test bestätigt) legt der erste Start einen Admin mit admin@example.com und password an. Ändern Sie beides sofort (Benutzermenü, Profile) oder setzen Sie ADMIN_EMAIL und ADMIN_PASSWORD vor dem allerersten Start, später wirken sie nicht mehr.
Bei verlorenem Zugang:
$ docker compose exec -it speedtest-tracker bash -c 'cd /app/www && php artisan app:user-reset-password'
Laut Projekt-FAQ wählen Sie eine weniger belegte Minute statt der vollen Stunde, falls geplante Messungen schlechter ausfallen als manuelle (Beispiel: Minute 17). Kontrolle:
$ docker compose exec speedtest-tracker bash -c 'cd /app/www && php artisan config:show speedtest'
Im Test: schedule ... 17 */6 * * *. Für feste Messserver listen Sie nahe Server auf und tragen die IDs in SPEEDTEST_SERVERS ein:
$ docker compose exec speedtest-tracker bash -c 'cd /app/www && php artisan app:ookla-list-servers'
Die erste Messung starten Sie in der oberen Leiste, im Test stand sie nach rund 15 Sekunden auf completed.
Die Anmeldung gelingt nur noch mit Ihren neuen Zugangsdaten, config:show speedtest zeigt Ihren Zeitplan, und die erste Messung steht in der Liste auf completed.
Benachrichtigungen und Schwellwerte
Kernkanäle sind laut Doku Datenbank, Mail und Webhook, weitere Dienste laufen über Apprise, andere Kanäle gelten als veraltet. Mail braucht SMTP-Variablen:
MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=speedtest@example.com
MAIL_PASSWORD=HIER_SMTP_PASSWORT
MAIL_FROM_ADDRESS=speedtest@example.com
MAIL_FROM_NAME="Speedtest Tracker"
Setzen Sie Variablen nicht gleichzeitig in einer .env der Anwendung und in der Compose-Datei (Doku). Schwellwerte (THRESHOLD_ENABLED, THRESHOLD_DOWNLOAD, THRESHOLD_UPLOAD, THRESHOLD_PING) wirken als Variablen nur bei der Ersteinrichtung, danach pflegen Sie sie in der Oberfläche. Eigene Push-Dienste binden Sie per Webhook oder Apprise an.
Eine Testbenachrichtigung aus der Oberfläche kommt im gewählten Kanal an.
Funktionsprüfung und Healthcheck
Der dokumentierte Endpunkt /api/healthcheck antwortet mit HTTP 200 und einer kurzen JSON-Meldung:
$ curl -s http://127.0.0.1:18472/api/healthcheck# Ausgabe im Test:
# {"message":"Speedtest Tracker is running!"}
Der Healthcheck in der compose.yaml nutzt denselben Endpunkt (curl ist im Image). Die REST-API unter /api/v1 verlangt ein Token, ohne Token kam im Test HTTP 401. Tokens legen Sie unter /admin/api-tokens an.
curl -s http://127.0.0.1:18472/api/healthcheck liefert {"message":"Speedtest Tracker is running!"}, /api/v1 ohne Token HTTP 401.
Netzwerkfreigabe, Reverse Proxy und TLS
Die Bindung an 127.0.0.1 macht die Oberfläche nur vom Host erreichbar (LAN-Adresse schlug im Test fehl). Setzen Sie für Netzzugriff einen Reverse Proxy mit gültigem Zertifikat davor und tragen Sie die öffentliche Adresse in APP_URL und ASSET_URL ein:
APP_URL=https://speedtest.example.com
ASSET_URL=https://speedtest.example.com
Ein passender Server-Block für Nginx nach dem Muster der Projektdokumentation:
server {
listen 443 ssl;
server_name speedtest.example.com;
ssl_certificate /etc/letsencrypt/live/speedtest.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/speedtest.example.com/privkey.pem;
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:18472;
}
}
Mit ALLOWED_IPS beschränken Sie Anfragen auf bestimmte Adressen, PUBLIC_DASHBOARD (Dashboard ohne Anmeldung) nur im internen Netz. Nutzen Sie VPN oder Proxy mit Anmeldung.
curl -I https://speedtest.example.com/admin/login liefert HTTP 200, die Seite lädt mit Styles, und ss -ltn | grep 18472 zeigt nur 127.0.0.1.
Persistente Daten und Rechte
Dank PUID und PGID gehören die Dateien in ./data dem Host-Benutzer, Unterverzeichnisse des Containers können abweichen: Im Test scheiterte rm -rf data an data/.composer/cache/.htaccess: Permission denied. Für Backup unkritisch, zum Entfernen brauchen Sie sudo oder einen Hilfscontainer.
Backup und Restore
Bei SQLite ist der Bestand eine Datei: Stoppen Sie den Container kurz und sichern Sie ihn. Sichern Sie die .env getrennt und geschützt, ohne passenden APP_KEY sind verschlüsselte Inhalte unlesbar.
# Container anhalten$ docker compose stop# Datenverzeichnis sichern$ tar czf speedtest-backup-$(date +%F).tgz -C data .# Container wieder starten$ docker compose start
Restore im Test: Markertabelle angelegt, Backup (12,5 KB), database.sqlite gelöscht, Backup zurückgespielt. SHA-256 stimmte, Healthcheck 200, Markertabelle zurück.
# Container anhalten$ docker compose stop# Datenbank aus dem Backup zurückholen$ tar xzf speedtest-backup-2026-09-26.tgz -C data ./database.sqlite# Prüfsumme mit dem Stand vor dem Backup vergleichen$ sha256sum data/database.sqlite# Container starten und Healthcheck abwarten$ docker compose start
$ curl -s http://127.0.0.1:18472/api/healthcheck
Beim Entpacken in ein nicht leeres Verzeichnis meldete tar Cannot open: File exists für Dateien unter .composer. Entpacken Sie in ein leeres Verzeichnis oder stellen Sie gezielt Dateien her. Bei MariaDB oder Postgres sichern Sie per mariadb-dump oder pg_dump.
tar tzf speedtest-backup-*.tgz | grep database.sqlite findet die Datenbank, nach dem Restore liefert der Healthcheck HTTP 200 und die Messungen sind wieder sichtbar.
Updates und Rollback-Grenzen
Update: Backup, Tag anheben, Container neu erstellen.
# neuen Tag in compose.yaml eintragen, danach:$ docker compose pull
$ docker compose up -d
$ docker compose logs --no-log-prefix | grep -i version
Beim Start laufen Migrationen, ein älteres Image kommt mit dem neuen Schema nicht zwingend zurecht. Rollback heißt: alter Tag plus Backup vor dem Update. Lesen Sie vor Versionssprüngen die Release-Hinweise.
Das Log nennt den neuen Tag, docker compose ps zeigt (healthy), die bisherigen Messungen sind vorhanden.
Typische Fehler mit Diagnose und Lösung
| Symptom | Ursache | Lösung |
|---|---|---|
Log zeigt An application key is missing, halting init!: Port antwortet nicht, Container bleibt trotzdem Up | APP_KEY leer oder nicht in der .env | Schlüssel erzeugen, eintragen, docker compose up -d. Im Test provoziert. |
Unsupported cipher or incorrect key length | Schlüssel falsch formatiert, etwa ohne base64: | Schlüssel mit dem dokumentierten Befehl neu erzeugen |
Bind for 127.0.0.1:18472 failed: port is already allocated | Port bereits belegt | Anderen Port wählen oder belegenden Dienst finden mit ss -ltnp. Im Test provoziert. |
Failed to connected to hostname oder Failed to fetch external IP address | Kein Internet im Container oder icanhazip.com per DNS-Filter blockiert | Domain freigeben oder eigene Adressen per SPEEDTEST_INTERNET_CHECK_HOSTNAME und SPEEDTEST_EXTERNAL_IP_URL setzen |
No servers defined oder Failed to find a working test server | Eingetragene Server-ID existiert nicht mehr | Liste mit app:ookla-list-servers neu holen, IDs anpassen |
| Geplante Messungen zu falscher Uhrzeit | Anwendung rechnet in UTC | APP_TIMEZONE und DISPLAY_TIMEZONE passend setzen, bei externer Datenbank deren Zeitzone angleichen |
| Oberfläche hinter Proxy ohne Styles | ASSET_URL fehlt | APP_URL und ASSET_URL auf die HTTPS-Adresse setzen |
500 | SERVER ERROR | Fehlkonfiguration oder Fehler in der Anwendung | Vorübergehend APP_DEBUG=true setzen, Log auf production.ERROR prüfen, danach wieder entfernen |
php artisan tinker fehlt im Image (Command "tinker" is not defined), about, config:show, schedule:list und die app:-Befehle stehen zur Verfügung.
Saubere Deinstallation
Achtung: Die Schritte löschen Messergebnisse, Benutzer und Einstellungen unwiderruflich. Sichern Sie vorher, wenn Sie die Messreihe als Beleg brauchen.
# Container und Netzwerk entfernen$ docker compose down -v --remove-orphans# Image namentlich löschen$ docker rmi lscr.io/linuxserver/speedtest-tracker:v1.15.0-ls172# Datenverzeichnis löschen, bei Rechtefehlern mit sudo$ sudo rm -rf data# Kontrolle$ docker ps -a --filter name=speedtest
Entfernen Sie auch Proxy-Block und DNS-Einträge.
Passende Anleitungen auf S-EDV
- Uptime Kuma mit Docker und Nginx Proxy Manager installieren
- Nginx als Reverse Proxy mit TLS einrichten
- ntfy mit Docker selbst hosten
Quellen
- Speedtest Tracker Dokumentation: Using Docker Compose
- Speedtest Tracker Dokumentation: Environment Variables
- Speedtest Tracker Dokumentation: Authentication (Standardkonto)
- Speedtest Tracker Dokumentation: Error Messages
- GitHub: alexjustesen/speedtest-tracker
- GitHub: linuxserver/docker-speedtest-tracker
- Bundesnetzagentur: Internetgeschwindigkeit und Nachweisverfahren
Grundlagen dazu: Docker Compose absichern.


