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:
Jorijn Schrijvershof
2026-01-05 07:12:59 +01:00
parent 2bf04ce3f7
commit 7a181e4b1a
10 changed files with 897 additions and 3 deletions
+67
View File
@@ -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;
}
}
+55
View File
@@ -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')"