mirror of
https://github.com/jorijn/meshcore-stats.git
synced 2026-08-11 19:22:45 +02:00
feat: add Docker containerization with GitHub Actions CI/CD
- Multi-stage Dockerfile with Python 3.12 + Ofelia scheduler - docker-compose.yml for production (ghcr.io image) - docker-compose.development.yml for local builds - GitHub Actions workflow for multi-arch builds (amd64/arm64) - Security hardening: non-root user, cap_drop, read_only filesystem - Trivy vulnerability scanning and SBOM generation - Nightly rebuilds for OS security patches 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# nginx configuration for MeshCore Stats static site
|
||||
# This file is used by the nginx container in docker-compose.yml
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index day.html index.html;
|
||||
|
||||
# UTF-8 charset for all text files
|
||||
charset utf-8;
|
||||
charset_types text/plain text/css text/javascript application/json image/svg+xml;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/javascript application/json image/svg+xml;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# HTML, JSON, TXT files - no cache (frequently updated)
|
||||
location ~* \.(html|json|txt)$ {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
# Re-add security headers (add_header in location blocks replaces parent)
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
}
|
||||
|
||||
# PNG files - no cache (charts are regenerated frequently)
|
||||
location ~* \.png$ {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
}
|
||||
|
||||
# CSS, JS, SVG files - short cache (5 minutes)
|
||||
location ~* \.(css|js|svg)$ {
|
||||
expires 5m;
|
||||
add_header Cache-Control "public, max-age=300" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
}
|
||||
|
||||
# Default location
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
# Ofelia Job Scheduler Configuration
|
||||
# https://github.com/mcuadros/ofelia
|
||||
#
|
||||
# This file defines the cron-like schedule for all MeshCore Stats tasks.
|
||||
# Jobs run inside the same container (job-local).
|
||||
|
||||
[global]
|
||||
# Save last run state for job status
|
||||
save = true
|
||||
|
||||
# =============================================================================
|
||||
# Data Collection Jobs
|
||||
# =============================================================================
|
||||
|
||||
[job-local "collect-companion"]
|
||||
# Collect metrics from companion node (USB serial)
|
||||
schedule = @every 1m
|
||||
command = python /app/scripts/collect_companion.py
|
||||
no-overlap = true
|
||||
|
||||
[job-local "collect-repeater"]
|
||||
# Collect metrics from repeater node (via LoRa)
|
||||
# Offset by 1 second to avoid USB serial conflicts with companion collection
|
||||
schedule = 1 1,16,31,46 * * * *
|
||||
command = python /app/scripts/collect_repeater.py
|
||||
no-overlap = true
|
||||
|
||||
# =============================================================================
|
||||
# Rendering Jobs
|
||||
# =============================================================================
|
||||
|
||||
[job-local "render-charts"]
|
||||
# Generate SVG charts from database
|
||||
schedule = @every 5m
|
||||
command = python /app/scripts/render_charts.py
|
||||
|
||||
[job-local "render-site"]
|
||||
# Generate static HTML site
|
||||
schedule = @every 5m
|
||||
command = python /app/scripts/render_site.py
|
||||
|
||||
[job-local "render-reports"]
|
||||
# Generate monthly/yearly statistics reports
|
||||
schedule = @daily
|
||||
command = python /app/scripts/render_reports.py
|
||||
|
||||
# =============================================================================
|
||||
# Maintenance Jobs
|
||||
# =============================================================================
|
||||
|
||||
[job-local "db-maintenance"]
|
||||
# Database VACUUM and ANALYZE for optimal performance
|
||||
# Runs at 3 AM on the 1st of each month
|
||||
schedule = 0 3 1 * *
|
||||
command = python -c "import sqlite3; db=sqlite3.connect('/data/state/metrics.db'); db.execute('VACUUM'); db.execute('ANALYZE'); db.close(); print('Database maintenance complete')"
|
||||
Reference in New Issue
Block a user