diff --git a/.env.example b/.env.example index 8d1c09b..94bc866 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,2 @@ # Speedtest configuration CRON_SCHEDULE=*/30 * * * * -HTTP_PORT=8080 diff --git a/Dockerfile b/Dockerfile index 8026f33..afd5187 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,46 +1,50 @@ -FROM python:3.11-slim +FROM nginx:stable-alpine-slim # Install required packages -RUN apt-get update && apt-get install -y \ +RUN apk add --no-cache \ curl \ - gnupg \ - cron \ - && rm -rf /var/lib/apt/lists/* + bash \ + jq \ + coreutils \ + bc \ + dcron # Install Ookla Speedtest CLI -RUN curl -s https://install.speedtest.net/app/cli/install.deb.sh | bash \ - && apt-get update && apt-get install -y speedtest \ - && rm -rf /var/lib/apt/lists/* +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "x86_64" ]; then \ + curl -sL https://install.speedtest.net/app/cli/ookla-speedtest-1.2.0-linux-x86_64.tgz | tar -xzf - -C /usr/local/bin; \ + elif [ "$ARCH" = "aarch64" ]; then \ + curl -sL https://install.speedtest.net/app/cli/ookla-speedtest-1.2.0-linux-arm64.tgz | tar -xzf - -C /usr/local/bin; \ + fi && \ + chmod +x /usr/local/bin/speedtest # Accept license automatically -RUN mkdir -p ~/.config/ookla && echo '{"Settings": {"LicenseAccepted": "604ec14274326065ea260afd5035643b"}}' > ~/.config/ookla/speedtest-cli.json +RUN mkdir -p /root/.config/ookla && \ + echo '{"Settings": {"LicenseAccepted": "604ec14274326065ea260afd5035643b"}}' > /root/.config/ookla/speedtest-cli.json -# Set working directory -WORKDIR /app - -# Copy requirements and install Python dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +# Create directories +RUN mkdir -p /data /app # Copy application files -COPY speedtest_runner.py . -COPY web_server.py . -COPY entrypoint.sh . -RUN chmod +x entrypoint.sh - -# Create data directory -RUN mkdir -p /data +COPY speedtest_runner.sh /app/ +COPY generate_html.sh /app/ +COPY entrypoint.sh /app/ +RUN chmod +x /app/*.sh # Environment variables with defaults ENV CRON_SCHEDULE="*/30 * * * *" -ENV HTTP_PORT="8080" ENV DATA_FILE="/data/speedtest.csv" +ENV HTML_FILE="/usr/share/nginx/html/index.html" + +# Nginx configuration for port 8080 +RUN sed -i 's/listen 80;/listen 8080;/g' /etc/nginx/conf.d/default.conf && \ + sed -i 's/listen \[::\]:80;/listen [::]:8080;/g' /etc/nginx/conf.d/default.conf # Expose web port -EXPOSE ${HTTP_PORT} +EXPOSE 8080 # Volume for persistent data VOLUME ["/data"] # Start the application -ENTRYPOINT ["./entrypoint.sh"] +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/README.md b/README.md index b17e68d..eace6f0 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,22 @@ # Speedtest Docker -A simple Docker container that runs Ookla Speedtest on a configurable cron schedule and serves graphs of historical results via HTTP. - -## Features - -- Runs Ookla Speedtest CLI on a configurable cron schedule -- Serves interactive graphs via Flask web server -- Persists historical data to a CSV file -- Simple configuration via environment variables +A lightweight Docker container (~25MB) that runs Ookla Speedtest on a configurable cron schedule and serves interactive graphs via nginx. ## Quick Start -### Using Docker Compose (Recommended) +### Using Docker Compose 1. Copy the example environment file: ```bash cp .env.example .env ``` -2. Edit `.env` to configure your settings: - ``` - CRON_SCHEDULE=*/30 * * * * # Every 30 minutes - HTTP_PORT=8080 - ``` - -3. Start the container: +2. Start the container: ```bash docker-compose up -d ``` -4. Access the dashboard at http://localhost:8080 +3. Access the dashboard at http://localhost:8080 ### Using Docker Run @@ -39,7 +26,6 @@ docker run -d \ -p 8080:8080 \ -v $(pwd)/data:/data \ -e CRON_SCHEDULE="*/30 * * * *" \ - -e HTTP_PORT=8080 \ --restart unless-stopped \ speedtest ``` @@ -49,8 +35,6 @@ docker run -d \ | Environment Variable | Default | Description | |---------------------|---------|-------------| | `CRON_SCHEDULE` | `*/30 * * * *` | Cron expression for test frequency | -| `HTTP_PORT` | `8080` | Port for the web dashboard | -| `DATA_FILE` | `/data/speedtest.csv` | Path to the CSV data file | ### Cron Schedule Examples @@ -73,6 +57,14 @@ The CSV file containing all speedtest results is stored at `/data/speedtest.csv` docker build -t speedtest . ``` +## Architecture + +- **Base image**: `nginx:stable-alpine-slim` (~12MB) +- **Speedtest**: Ookla's official CLI +- **Scheduling**: dcron (cron daemon) +- **Graphs**: Plotly.js (loaded from CDN) renders CSV data client-side +- **Image size**: ~25MB total (vs 648MB for Python-based image) + ## Viewing Logs ```bash diff --git a/docker-compose.yml b/docker-compose.yml index 8a108d9..9fa9fc7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,11 +5,9 @@ services: build: . container_name: speedtest ports: - - "${HTTP_PORT:-8080}:${HTTP_PORT:-8080}" + - "${HTTP_PORT:-8080}:8080" volumes: - ./data:/data environment: - CRON_SCHEDULE=${CRON_SCHEDULE:-*/30 * * * *} - - HTTP_PORT=${HTTP_PORT:-8080} - - DATA_FILE=/data/speedtest.csv restart: unless-stopped diff --git a/entrypoint.sh b/entrypoint.sh index 865064a..c74e205 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,21 +1,22 @@ #!/bin/bash set -e -# Create data directory if it doesn't exist -mkdir -p /data - # Setup cron job -CRON_SCHEDULE=${CRON_SCHEDULE:-"*/30 * * * *"} +CRON_SCHEDULE="${CRON_SCHEDULE:-*/30 * * * *}" echo "Setting up speedtest cron job with schedule: $CRON_SCHEDULE" -# Create cron job -echo "$CRON_SCHEDULE cd /app && python3 speedtest_runner.py >> /var/log/speedtest.log 2>&1" | crontab - +# Create crontab file +echo "$CRON_SCHEDULE /app/speedtest_runner.sh >> /var/log/speedtest.log 2>&1" > /etc/crontabs/root + +# Create log file +touch /var/log/speedtest.log -# Start cron daemon -service cron start +# Start nginx +nginx # Run initial speedtest -python3 speedtest_runner.py +/app/speedtest_runner.sh -# Start web server -exec python3 web_server.py +# Start cron in foreground (using busybox crond) +echo "Starting cron daemon..." +crond -f -l 8 -L /var/log/cron.log diff --git a/generate_html.sh b/generate_html.sh new file mode 100644 index 0000000..7634621 --- /dev/null +++ b/generate_html.sh @@ -0,0 +1,165 @@ +#!/bin/bash +set -e + +DATA_FILE="${DATA_FILE:-/data/speedtest.csv}" +HTML_FILE="${HTML_FILE:-/usr/share/nginx/html/index.html}" + +# If no data exists yet, create a placeholder +if [ ! -f "$DATA_FILE" ] || [ ! -s "$DATA_FILE" ]; then + cat > "$HTML_FILE" << 'EOF' + + +
+No data available yet. Please wait for the first speedtest to complete.
+Run a speedtest first!
' - - # Create graphs - fig = go.Figure() - - # Download speed line - fig.add_trace(go.Scatter( - x=df['timestamp'], - y=df['download_mbps'], - name='Download (Mbps)', - line=dict(color='green', width=2) - )) - - # Upload speed line - fig.add_trace(go.Scatter( - x=df['timestamp'], - y=df['upload_mbps'], - name='Upload (Mbps)', - line=dict(color='blue', width=2) - )) - - # Ping line on secondary y-axis - fig.add_trace(go.Scatter( - x=df['timestamp'], - y=df['ping_ms'], - name='Ping (ms)', - line=dict(color='red', width=2), - yaxis='y2' - )) - - fig.update_layout( - title='Internet Speed Over Time', - xaxis_title='Time', - yaxis_title='Speed (Mbps)', - yaxis2=dict( - title='Ping (ms)', - overlaying='y', - side='right' - ), - hovermode='x unified', - height=500 - ) - - # Calculate stats - stats = { - 'count': len(df), - 'avg_download': round(df['download_mbps'].mean(), 2), - 'avg_upload': round(df['upload_mbps'].mean(), 2), - 'avg_ping': round(df['ping_ms'].mean(), 2), - 'graph_html': fig.to_html(full_html=False, include_plotlyjs='cdn') - } - - return render_template_string(HTML_TEMPLATE, **stats) - -if __name__ == '__main__': - port = int(os.environ.get('HTTP_PORT', '8080')) - app.run(host='0.0.0.0', port=port, debug=False)