diff --git a/.env.example b/.env.example index 5812e5d..ea0ee8c 100644 --- a/.env.example +++ b/.env.example @@ -32,8 +32,14 @@ MQTT_MAX_PAYLOAD_BYTES=65536 MQTT_INGEST_CONCURRENCY=8 MQTT_INGEST_QUEUE_MAX=1000 +# Archive and remove repeater visibility after its own observer MQTT feed has +# been silent for one month. Credentials and packet history are preserved. +STALE_MQTT_OBSERVER_CLEANUP_DAYS=30 +STALE_MQTT_OBSERVER_CLEANUP_INTERVAL_MS=21600000 + # Bound expensive statistics scans so they cannot exhaust the OLTP pool. STATS_DB_QUERY_CONCURRENCY=2 +WARMUP_NETWORKS=ukmesh # ── Redis ───────────────────────────────────────────────────────────────────── # Docker Compose protects the internal Redis service with this password. The diff --git a/Dockerfile b/Dockerfile index 8463df0..d70b344 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,7 @@ COPY --from=backend-builder /build/backend/dist ./dist COPY --from=backend-builder /build/backend/src/db/schema ./dist/db/schema COPY --from=backend-builder /build/backend/src/db/migrations ./dist/db/migrations COPY --from=backend-builder /build/backend/src/db/owner-auth.sql ./dist/db/owner-auth.sql +COPY --from=backend-builder /build/backend/src/backend-site/template.html ./dist/backend-site/template.html # Copy frontend build into static dir served by backend COPY --from=frontend-builder /build/frontend/dist ./public diff --git a/Dockerfile.backend b/Dockerfile.backend index 3a0fc24..a84fdbe 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -2,14 +2,14 @@ FROM node:20-alpine AS backend-builder WORKDIR /build/backend COPY backend/package.json backend/package-lock.json ./ -RUN npm ci +RUN npm ci --audit COPY backend/ ./ RUN npm run build FROM node:20-alpine AS runtime WORKDIR /app COPY backend/package.json backend/package-lock.json ./ -RUN npm ci --omit=dev && npm cache clean --force +RUN npm ci --omit=dev --audit && npm cache clean --force COPY --from=backend-builder /build/backend/dist ./dist COPY --from=backend-builder /build/backend/src/db/schema ./dist/db/schema COPY --from=backend-builder /build/backend/src/db/migrations ./dist/db/migrations diff --git a/docker-compose.yml b/docker-compose.yml index 9bebc12..6bf8ee3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +name: ${COMPOSE_PROJECT_NAME:-meshcore-analytics} + x-anubis-base: &anubis-base image: ghcr.io/techarohq/anubis:v1.25.0 restart: always @@ -203,6 +205,9 @@ services: volumes: - mosquitto_log:/mosquitto/log:ro - ./mosquitto:/mosquitto/config + # The backend currently needs Docker API access for the owner ACL + # reconciler. Production deployments should prefer docker-socket-proxy + # with only the required endpoints; use :ro when mutation is not needed. - /var/run/docker.sock:/var/run/docker.sock healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/healthz >/dev/null"] @@ -298,6 +303,8 @@ services: JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required} DATABASE_STATEMENT_TIMEOUT_MS: 300000 DATABASE_SKIP_SCHEMA_INIT: "true" + STALE_MQTT_OBSERVER_CLEANUP_DAYS: ${STALE_MQTT_OBSERVER_CLEANUP_DAYS:-30} + STALE_MQTT_OBSERVER_CLEANUP_INTERVAL_MS: ${STALE_MQTT_OBSERVER_CLEANUP_INTERVAL_MS:-21600000} NODE_ENV: production depends_on: timescaledb: @@ -338,7 +345,7 @@ services: build: context: . dockerfile: Dockerfile.backend - restart: "no" + restart: on-failure:3 logging: *json-log-limits mem_limit: "512m" memswap_limit: "512m" @@ -754,6 +761,23 @@ services: depends_on: - loki + prometheus: + <<: *internal-hosts + image: prom/prometheus:v3.5.0 + restart: always + logging: *json-log-limits + mem_limit: "256m" + memswap_limit: "256m" + command: ["--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.retention.time=15d"] + volumes: + - ./logging/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + ports: + - "127.0.0.1:9090:9090" + depends_on: + backend: + condition: service_healthy + grafana: <<: *internal-hosts image: grafana/grafana:11.5.0 @@ -773,6 +797,7 @@ services: - "127.0.0.1:3010:3000" depends_on: - loki + - prometheus networks: default: @@ -792,4 +817,5 @@ volumes: srtm_data: loki_data: grafana_data: + prometheus_data: mesh_health_check_data: diff --git a/nginx.app.conf b/nginx.app.conf index 209bddb..01fec5d 100644 --- a/nginx.app.conf +++ b/nginx.app.conf @@ -9,6 +9,11 @@ map "$mesh_trusted_edge:$http_cf_connecting_ip" $mesh_client_ip { ~^1:([0-9A-Fa-f:.]+)$ $1; } +map $uri $mesh_permissions_policy { + default "geolocation=(), camera=(), microphone=()"; + ~^/(feed|repeater)?/?$ "geolocation=(self), camera=(), microphone=()"; +} + server { listen 80; root /usr/share/nginx/html; diff --git a/nginx.security-headers.conf b/nginx.security-headers.conf index 3d26551..79d0383 100644 --- a/nginx.security-headers.conf +++ b/nginx.security-headers.conf @@ -2,7 +2,7 @@ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; prelo add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; -add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; +add_header Permissions-Policy "$mesh_permissions_policy" always; add_header Cross-Origin-Opener-Policy "same-origin" always; add_header Cross-Origin-Resource-Policy "same-origin" always; -add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self' wss: https:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'" always; +add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://basemaps.cartocdn.com https://*.basemaps.cartocdn.com https://openstreetmap.org https://*.openstreetmap.org; connect-src 'self' wss: https:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'" always; diff --git a/nginx.website.conf b/nginx.website.conf index 975acb5..99f6bff 100644 --- a/nginx.website.conf +++ b/nginx.website.conf @@ -9,6 +9,11 @@ map "$mesh_trusted_edge:$http_cf_connecting_ip" $mesh_client_ip { ~^1:([0-9A-Fa-f:.]+)$ $1; } +map $uri $mesh_permissions_policy { + default "geolocation=(), camera=(), microphone=()"; + ~^/(feed|repeater)?/?$ "geolocation=(self), camera=(), microphone=()"; +} + server { listen 80; root /usr/share/nginx/html;