[
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:        \n        description: \"The version to release\"\n        default: \"1.0.0\"      \n        required: true\n    \njobs:\n  release:\n    name: Update Container Images\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v1.10.0\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@master\n        with:\n          platforms: all\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n      \n      - run: |\n          docker buildx build \\\n            --push \\\n            --platform linux/arm/v7,linux/arm64/v8,linux/amd64 \\\n            --tag robinmanuelthiel/speedtest:${{ github.event.inputs.version }} \\\n            --tag robinmanuelthiel/speedtest:latest \\\n            .\n\n      - name: Create GitHub Release\n        uses: softprops/action-gh-release@v1\n        with:\n          tag_name: v${{ github.event.inputs.version }}\n          name: Version ${{ github.event.inputs.version }}\n          generate_release_notes: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  \n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM debian:bullseye\n\n# Install basics\nRUN apt-get update && apt-get install -y curl jq gnupg1 apt-transport-https dirmngr\n\n# Install speedtest cli\nRUN curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | bash\nRUN apt-get install -y speedtest\n\nCOPY ./speedtest.sh .\nCMD [\"./speedtest.sh\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Robin-Manuel Thiel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Internet Speed Test in a Container\n\n[![Docker](https://img.shields.io/badge/Docker%20Hub-robinmanuelthiel/speedtest-blue.svg?logo=docker)](https://hub.docker.com/r/robinmanuelthiel/speedtest/)\n\nCheck your internet bandwidth using the [Speedtest CLI](https://www.speedtest.net/apps/cli) from a Docker container. You can configure the tool to run periodically and save the results to an InfluxDB for visualization or long-term records.\n\n```bash\ndocker run --rm robinmanuelthiel/speedtest:latest\n```\n\nThe result will then look like this:\n\n```bash\nRunning a Speed Test...\nYour download speed is 334 Mbps (29284399 Bytes/s).\nYour upload speed is 42 Mbps (4012944 Bytes/s).\nYour ping is 6.223 ms.\nSpeedtest took 23 seconds.\"\n```\n\n## Configuration\n\n| Environment variable  | Default value           | Description                                                                          |\n|-----------------------|-------------------------|--------------------------------------------------------------------------------------|\n| `LOOP`                | `false`                 | Run Speedtest in a loop                                                              |\n| `LOOP_DELAY`          | `60`                    | Delay in seconds between the runs                                                    |\n| `DB_SAVE`             | `false`                 | Save values to InfluxDB                                                              |\n| `DB_HOST`             | `http://localhost:8086` | InfluxDB Hostname                                                                    |\n| `DB_NAME`             | `speedtest`             | InfluxDB Database name                                                               |\n| `DB_USERNAME`         | `admin`                 | InfluxDB Username                                                                    |\n| `DB_PASSWORD`         | `password`              | InfluxDB Password                                                                    |\n| `SPEEDTEST_SERVER_ID` | none                    | Specify a server from the server list using its ID                                   |\n| `SPEEDTEST_HOSTNAME`  | none                    | Specify a server, from the server list, using its host's fully qualified domain name |\n\nTo get the available Server IDs, sponsors, and hostnames from speedtest run:\n\n```bash\ncurl -s https://cli.speedtest.net/api/cli/config | jq -r '.servers[] | \"id: \\(.id), sponsor: \\(.sponsor), host: \\(.host)\"'\n```\n\n## Grafana and InfluxDB\n\n![Screenshot of a Grafana Dashboard with upload and download speed values](img/grafana.png)\n\nFor a full visualization and long term tracking, I recommend InfluxDB as a time-series database and Grafana as a dashboard engine. Both come in Docker containers, so the whole setup can be achieved by starting a Docker Compose file.\n\n```yaml\nversion: \"3\"\nservices:\n  grafana:\n    image: grafana/grafana:7.5.2\n    restart: always\n    ports:\n      - 3000:3000\n    volumes:\n      - grafana:/var/lib/grafana\n    depends_on:\n      - influxdb\n\n  influxdb:\n    image: influxdb:1.8.3\n    restart: always\n    volumes:\n      - influxdb:/var/lib/influxdb\n    ports:\n      - 8083:8083\n      - 8086:8086\n    environment:\n      - INFLUXDB_ADMIN_USER=\"admin\"\n      - INFLUXDB_ADMIN_PASSWORD=\"password\"\n      - INFLUXDB_DB=\"speedtest\"\n\n  speedtest:\n    image: robinmanuelthiel/speedtest:latest\n    restart: always\n    environment:\n      - LOOP=true\n      - LOOP_DELAY=1800\n      - DB_SAVE=true\n      - DB_HOST=http://influxdb:8086\n      - DB_NAME=speedtest\n      - DB_USERNAME=admin\n      - DB_PASSWORD=password\n    privileged: true # Needed for 'sleep' in the loop\n    depends_on:\n      - influxdb\n\nvolumes:\n  grafana:\n  influxdb:\n```\n\nTo configure Grafana, we need to **add InfluxDB as a data source first** and then create a dashboard with the upload and download values. You can find a demo dashboard configuration in the [/demo](/demo) folder.\n\n> **Hint:** The speedtest outputs values as bytes per second. Make sure to divide all values by 125000 in your dashboard to get the Mbps values.\n"
  },
  {
    "path": "demo/dashboard.json",
    "content": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"gnetId\": null,\n  \"graphTooltip\": 0,\n  \"id\": 1,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"cacheTimeout\": null,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": null,\n      \"fill\": 1,\n      \"fillGradient\": 2,\n      \"gridPos\": {\n        \"h\": 13,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"hiddenSeries\": false,\n      \"id\": 2,\n      \"legend\": {\n        \"alignAsTable\": false,\n        \"avg\": true,\n        \"current\": false,\n        \"hideEmpty\": false,\n        \"max\": true,\n        \"min\": true,\n        \"rightSide\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": true\n      },\n      \"lines\": true,\n      \"linewidth\": 2,\n      \"links\": [],\n      \"nullPointMode\": \"connected\",\n      \"options\": {\n        \"dataLinks\": []\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"6.7.2\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"alias\": \"download\",\n          \"groupBy\": [\n            {\n              \"params\": [\n                \"$__interval\"\n              ],\n              \"type\": \"time\"\n            },\n            {\n              \"params\": [\n                \"null\"\n              ],\n              \"type\": \"fill\"\n            }\n          ],\n          \"hide\": false,\n          \"measurement\": \"download\",\n          \"orderByTime\": \"ASC\",\n          \"policy\": \"default\",\n          \"query\": \"SELECT mean(\\\"value\\\") FROM \\\"download\\\" WHERE $timeFilter GROUP BY time($__interval) fill(null)\",\n          \"rawQuery\": false,\n          \"refId\": \"A\",\n          \"resultFormat\": \"time_series\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"field\"\n              },\n              {\n                \"params\": [],\n                \"type\": \"mean\"\n              },\n              {\n                \"params\": [\n                  \" / 125000\"\n                ],\n                \"type\": \"math\"\n              }\n            ]\n          ],\n          \"tags\": []\n        },\n        {\n          \"alias\": \"upload\",\n          \"groupBy\": [\n            {\n              \"params\": [\n                \"$__interval\"\n              ],\n              \"type\": \"time\"\n            },\n            {\n              \"params\": [\n                \"null\"\n              ],\n              \"type\": \"fill\"\n            }\n          ],\n          \"measurement\": \"upload\",\n          \"orderByTime\": \"ASC\",\n          \"policy\": \"default\",\n          \"refId\": \"B\",\n          \"resultFormat\": \"time_series\",\n          \"select\": [\n            [\n              {\n                \"params\": [\n                  \"value\"\n                ],\n                \"type\": \"field\"\n              },\n              {\n                \"params\": [],\n                \"type\": \"mean\"\n              },\n              {\n                \"params\": [\n                  \"/125000\"\n                ],\n                \"type\": \"math\"\n              }\n            ]\n          ],\n          \"tags\": []\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"m-Net\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"Mbits\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    }\n  ],\n  \"refresh\": \"5m\",\n  \"schemaVersion\": 22,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-12h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ]\n  },\n  \"timezone\": \"\",\n  \"title\": \"Bandwidth\",\n  \"uid\": \"M27juGmgk\",\n  \"variables\": {\n    \"list\": []\n  },\n  \"version\": 5\n}\n"
  },
  {
    "path": "demo/docker-compose.yaml",
    "content": "version: \"3\"\nservices:\n  grafana:\n    image: grafana/grafana:7.5.2\n    restart: always\n    ports:\n      - 3000:3000\n    volumes:\n      - grafana:/var/lib/grafana\n    depends_on:\n      - influxdb\n\n  influxdb:\n    image: influxdb:1.8.3\n    restart: always\n    volumes:\n      - influxdb:/var/lib/influxdb\n    ports:\n      - 8083:8083\n      - 8086:8086\n    environment:\n      - INFLUXDB_ADMIN_USER=\"admin\"\n      - INFLUXDB_ADMIN_PASSWORD=\"password\"\n      - INFLUXDB_DB=\"speedtest\"\n\n  speedtest:\n    image: robinmanuelthiel/speedtest:latest\n    build: \n      context: ../\n      dockerfile: Dockerfile\n    restart: always\n    environment:\n      - LOOP=true\n      - LOOP_DELAY=300\n      - DB_SAVE=true\n      - DB_HOST=http://influxdb:8086\n      - DB_NAME=speedtest\n      - DB_USERNAME=admin\n      - DB_PASSWORD=password\n    privileged: true # Needed for 'sleep' in the loop\n    depends_on:\n      - influxdb\n\nvolumes:\n  grafana:\n  influxdb:\n"
  },
  {
    "path": "speedtest.sh",
    "content": "#!/bin/sh\n# These values can be overwritten with env variables\nLOOP=\"${LOOP:-false}\"\nLOOP_DELAY=\"${LOOP_DELAY:-60}\"\nDB_SAVE=\"${DB_SAVE:-false}\"\nDB_HOST=\"${DB_HOST:-http://localhost:8086}\"\nDB_NAME=\"${DB_NAME:-speedtest}\"\nDB_USERNAME=\"${DB_USERNAME:-admin}\"\nDB_PASSWORD=\"${DB_PASSWORD:-password}\"\nSPEEDTEST_HOSTNAME=\"${SPEEDTEST_HOSTNAME}\"\nSPEEDTEST_SERVER_ID=\"${SPEEDTEST_SERVER_ID}\"\n\nrun_speedtest()\n{\n    DATE=$(date +%s)\n    HOSTNAME=$(hostname)\n    START_TIME=$(date +%s)\n\n    # Start speed test\n    if [ -n \"$SPEEDTEST_SERVER_ID\" ]; then\n        echo \"Running a Speed Test with Server ID $SPEEDTEST_SERVER_ID... \"\n        JSON=$(speedtest --accept-license --accept-gdpr -f json -s $SPEEDTEST_SERVER_ID) || JSON=\"\"\n    elif [ -n \"$SPEEDTEST_HOSTNAME\" ]; then\n        echo \"Running a Speed Test with Hostname $SPEEDTEST_HOSTNAME... \"\n        JSON=$(speedtest --accept-license --accept-gdpr -f json -o $SPEEDTEST_HOSTNAME) || JSON=\"\"\n    else\n        echo \"Running a Speed Test with default host... \"\n        JSON=$(speedtest --accept-license --accept-gdpr -f json) || JSON=\"\"\n    fi\n\n    END_TIME=$(date +%s)\n    DURATION=$((END_TIME - START_TIME))    \n\n    # If JSON is empty, then the speedtest command failed.\n    # In this case set the values to 0.\n    if [ -z \"$JSON\" ]; then\n        DOWNLOAD=0\n        UPLOAD=0\n        PING=0\n    else\n        # Fetch the values from the JSON output and convert them to the correct units (converts null to 0)\n        DOWNLOAD=\"$(echo $JSON | jq -r '.download.bandwidth // 0')\"\n        UPLOAD=\"$(echo $JSON | jq -r '.upload.bandwidth // 0')\"\n        PING=\"$(echo $JSON | jq -r '.ping.latency // 0')\"\n    fi\n\n    echo \"Your download speed is $(($DOWNLOAD / 125000 )) Mbps ($DOWNLOAD Bytes/s).\"\n    echo \"Your upload speed is $(($UPLOAD / 125000 )) Mbps ($UPLOAD Bytes/s).\"\n    echo \"Your ping is $PING ms.\"\n    echo \"Speedtest took $DURATION seconds.\"\n\n    # Save results in the database\n    if $DB_SAVE; \n    then\n        echo \"Saving values to database...\"\n        curl -s -S -XPOST \"$DB_HOST/write?db=$DB_NAME&precision=s&u=$DB_USERNAME&p=$DB_PASSWORD\" \\\n            --data-binary \"download,host=$HOSTNAME value=$DOWNLOAD $DATE\"\n        curl -s -S -XPOST \"$DB_HOST/write?db=$DB_NAME&precision=s&u=$DB_USERNAME&p=$DB_PASSWORD\" \\\n            --data-binary \"upload,host=$HOSTNAME value=$UPLOAD $DATE\"\n        curl -s -S -XPOST \"$DB_HOST/write?db=$DB_NAME&precision=s&u=$DB_USERNAME&p=$DB_PASSWORD\" \\\n            --data-binary \"ping,host=$HOSTNAME value=$PING $DATE\"\n        echo \"Values saved.\"\n    fi\n}\n\n# Check for input errors\nif [ -n \"$SPEEDTEST_SERVER_ID\" ] && [ -n \"$SPEEDTEST_HOSTNAME\" ]; then\n    echo >&2 \"[error] Only one server option can be specified, please use one of ['SPEEDTEST_SERVER_ID' or 'SPEEDTEST_HOSTNAME']\"\n    exit 1\nfi\n\nif $LOOP;\nthen\n    echo \"Running speedtest in a loop until stopped...\"\n    while :\n    do\n        run_speedtest\n        echo \"Running next test in ${LOOP_DELAY}s...\"\n        sleep $LOOP_DELAY\n    done\nelse\n    run_speedtest\nfi\n"
  }
]