2026-07-07 17:50:51 -04:00
|
|
|
# OSINT Dashboard — container stack (all env-driven, 12-factor).
|
|
|
|
|
# Single combined TimescaleDB+PostGIS image is pinned to pg13 because that is
|
|
|
|
|
# the only arm64 build Timescale publishes with PostGIS bundled. The app uses
|
|
|
|
|
# only standard SQL types, so pg13 is fully sufficient.
|
|
|
|
|
#
|
|
|
|
|
# Bring up: docker compose up -d
|
|
|
|
|
# With NATS: docker compose --profile ingest up -d
|
|
|
|
|
# Env: copy .env.example to .env and adjust.
|
2026-08-24 17:58:50 -04:00
|
|
|
#
|
|
|
|
|
# Project name is pinned here (NOT via COMPOSE_PROJECT_NAME in CI) so the
|
|
|
|
|
# named volumes stay osint-dashboard_osint-pgdata / _camera-snapshots and
|
|
|
|
|
# match the live stack on the Pi.
|
|
|
|
|
name: osint-dashboard
|
2026-07-07 17:50:51 -04:00
|
|
|
|
|
|
|
|
services:
|
|
|
|
|
db:
|
2026-08-27 18:26:57 -04:00
|
|
|
# NO build: block here on purpose. The DB image (TimescaleDB+PostGIS) is
|
|
|
|
|
# built once via Dockerfile.pg and pinned as localhost/osint-dashboard-pg.
|
|
|
|
|
# Dockerfile.pg installs TimescaleDB from packagecloud.io, which some
|
|
|
|
|
# networks (and ISP abuse-mitigation blackholes) block, so rebuilding it
|
|
|
|
|
# on every CI deploy made the pipeline flaky. Rebuild manually when the
|
|
|
|
|
# base image or extensions need bumping:
|
|
|
|
|
# docker compose build db && docker compose up -d db
|
2026-07-07 18:00:45 -04:00
|
|
|
image: localhost/osint-dashboard-pg:latest
|
2026-07-07 17:50:51 -04:00
|
|
|
container_name: osint-db
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
environment:
|
|
|
|
|
POSTGRES_USER: ${DB_USER:-osint}
|
|
|
|
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-osint}
|
|
|
|
|
POSTGRES_DB: ${DB_NAME:-osint_data}
|
2026-07-07 18:21:48 -04:00
|
|
|
# Ensure TimescaleDB is preloaded (conf.d drop-in may be ignored by the
|
|
|
|
|
# official image's runtime-generated postgresql.conf, so pass it explicitly).
|
|
|
|
|
command: ["-c", "shared_preload_libraries=timescaledb"]
|
2026-07-07 17:50:51 -04:00
|
|
|
ports:
|
|
|
|
|
- "127.0.0.1:5432:5432"
|
|
|
|
|
volumes:
|
|
|
|
|
- osint-pgdata:/var/lib/postgresql/data
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-osint} -d ${DB_NAME:-osint_data}"]
|
|
|
|
|
interval: 10s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 10
|
|
|
|
|
|
|
|
|
|
nats:
|
|
|
|
|
image: nats:2.10
|
2026-07-07 18:00:45 -04:00
|
|
|
platform: linux/arm64
|
2026-07-07 17:50:51 -04:00
|
|
|
container_name: osint-nats
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
profiles: ["ingest"]
|
|
|
|
|
ports:
|
|
|
|
|
- "127.0.0.1:4222:4222"
|
|
|
|
|
- "127.0.0.1:8222:8222"
|
|
|
|
|
command: ["-js"]
|
|
|
|
|
|
2026-07-07 19:18:37 -04:00
|
|
|
ingester:
|
|
|
|
|
build:
|
|
|
|
|
context: .
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
platforms: ["linux/arm64"]
|
|
|
|
|
image: localhost/osint-dashboard:latest
|
|
|
|
|
container_name: osint-ingester
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
profiles: ["ingest"]
|
|
|
|
|
depends_on:
|
|
|
|
|
nats:
|
|
|
|
|
condition: service_started
|
|
|
|
|
db:
|
|
|
|
|
condition: service_healthy
|
|
|
|
|
environment:
|
|
|
|
|
DB_USER: ${DB_USER:-osint}
|
|
|
|
|
DB_PASSWORD: ${DB_PASSWORD:-osint}
|
|
|
|
|
DB_HOST: db
|
|
|
|
|
DB_PORT: ${DB_PORT:-5432}
|
|
|
|
|
DB_NAME: ${DB_NAME:-osint_data}
|
|
|
|
|
NATS_URL: ${NATS_URL:-nats://nats:4222}
|
|
|
|
|
RSS_URL: ${RSS_URL:-}
|
|
|
|
|
GDELT_QUERY: ${GDELT_QUERY:-}
|
|
|
|
|
INGEST_INTERVAL: ${INGEST_INTERVAL:-300}
|
|
|
|
|
INGEST_EARTHQUAKES: ${INGEST_EARTHQUAKES:-1}
|
Add NASA FIRMS active-fire ingest + /api/fires; API keys management page
Coherent merge of two coordinated features on the shared working tree:
FIRMS fire heatmap (backend, t_6e404c14):
- app/fire_sources.py: fetch FIRMS VIIRS area CSV (free MAP_KEY) -> NATS events.fire
- fires hypertable (TimescaleDB, 1-day chunks) with natural-key PK
(latitude, longitude, acq_time, satellite); idempotent ON CONFLICT DO NOTHING
- alembic/versions/002_fires.py; GET /api/fires?bbox=&since= (JSON only)
- POST /api/ingest/fires; ~15 min poll loop (FIRMS_INTERVAL=900) in ingester
- env-driven config (FIRMS_MAP_KEY/DATASET/BBOX/INTERVAL); docs/firms.md covers
the zero-cost GIBS VIIRS_SNPP_Thermal_Anomalies_375m_All tile alternative
- 18 tests (parser, mapping, idempotency, API contract) verified vs real
TimescaleDB+PostGIS (localhost/osint-dashboard-pg image)
API keys page (frontend, t_4433cff2):
- app/keystore.py: api_keys table (self-creating), FIRMS/GEMINI/TELEGRAM
registry with format validation, ****last4 masking, get_api_key()
- GET/POST/DELETE /api/keys (never returns full values); Keys tab in index.html
DB_NULL_POOL env switch in app/database.py enables a NullPool for tests /
short-lived processes that open a fresh event loop per unit.
2026-08-24 15:37:42 -04:00
|
|
|
# ── NASA FIRMS active fires ──
|
|
|
|
|
INGEST_FIRES: ${INGEST_FIRES:-1}
|
|
|
|
|
FIRMS_MAP_KEY: ${FIRMS_MAP_KEY:-}
|
|
|
|
|
FIRMS_DATASET: ${FIRMS_DATASET:-VIIRS_SNPP_NRT}
|
|
|
|
|
FIRMS_BBOX: ${FIRMS_BBOX:--180,-60,180,75}
|
|
|
|
|
FIRMS_INTERVAL: ${FIRMS_INTERVAL:-900}
|
2026-07-07 19:18:37 -04:00
|
|
|
command: ["python", "app/run_ingester.py"]
|
2026-07-07 19:35:28 -04:00
|
|
|
entrypoint: ["python", "app/run_ingester.py"]
|
2026-07-07 19:18:37 -04:00
|
|
|
|
2026-07-07 17:50:51 -04:00
|
|
|
app:
|
|
|
|
|
build:
|
|
|
|
|
context: .
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
platforms: ["linux/arm64"]
|
|
|
|
|
image: localhost/osint-dashboard:latest
|
|
|
|
|
container_name: osint-dashboard
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
depends_on:
|
|
|
|
|
db:
|
|
|
|
|
condition: service_healthy
|
|
|
|
|
environment:
|
|
|
|
|
DB_USER: ${DB_USER:-osint}
|
|
|
|
|
DB_PASSWORD: ${DB_PASSWORD:-osint}
|
|
|
|
|
DB_HOST: db
|
|
|
|
|
DB_PORT: ${DB_PORT:-5432}
|
|
|
|
|
DB_NAME: ${DB_NAME:-osint_data}
|
|
|
|
|
NATS_URL: ${NATS_URL:-nats://nats:4222}
|
|
|
|
|
MINIO_ENDPOINT: ${MINIO_ENDPOINT:-minio:9000}
|
|
|
|
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-}
|
|
|
|
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-}
|
|
|
|
|
MINIO_SECURE: ${MINIO_SECURE:-false}
|
Add NASA FIRMS active-fire ingest + /api/fires; API keys management page
Coherent merge of two coordinated features on the shared working tree:
FIRMS fire heatmap (backend, t_6e404c14):
- app/fire_sources.py: fetch FIRMS VIIRS area CSV (free MAP_KEY) -> NATS events.fire
- fires hypertable (TimescaleDB, 1-day chunks) with natural-key PK
(latitude, longitude, acq_time, satellite); idempotent ON CONFLICT DO NOTHING
- alembic/versions/002_fires.py; GET /api/fires?bbox=&since= (JSON only)
- POST /api/ingest/fires; ~15 min poll loop (FIRMS_INTERVAL=900) in ingester
- env-driven config (FIRMS_MAP_KEY/DATASET/BBOX/INTERVAL); docs/firms.md covers
the zero-cost GIBS VIIRS_SNPP_Thermal_Anomalies_375m_All tile alternative
- 18 tests (parser, mapping, idempotency, API contract) verified vs real
TimescaleDB+PostGIS (localhost/osint-dashboard-pg image)
API keys page (frontend, t_4433cff2):
- app/keystore.py: api_keys table (self-creating), FIRMS/GEMINI/TELEGRAM
registry with format validation, ****last4 masking, get_api_key()
- GET/POST/DELETE /api/keys (never returns full values); Keys tab in index.html
DB_NULL_POOL env switch in app/database.py enables a NullPool for tests /
short-lived processes that open a fresh event loop per unit.
2026-08-24 15:37:42 -04:00
|
|
|
# ── NASA FIRMS (for the /api/ingest/fires trigger endpoint) ──
|
|
|
|
|
FIRMS_MAP_KEY: ${FIRMS_MAP_KEY:-}
|
|
|
|
|
FIRMS_DATASET: ${FIRMS_DATASET:-VIIRS_SNPP_NRT}
|
|
|
|
|
FIRMS_BBOX: ${FIRMS_BBOX:--180,-60,180,75}
|
2026-07-07 17:50:51 -04:00
|
|
|
ports:
|
|
|
|
|
- "127.0.0.1:8000:8000"
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD-SHELL", "python -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/api/health').status==200 else 1)\""]
|
|
|
|
|
interval: 30s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 5
|
|
|
|
|
|
2026-08-24 14:54:51 -04:00
|
|
|
camera-service:
|
|
|
|
|
build:
|
|
|
|
|
context: .
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
platforms: ["linux/arm64"]
|
|
|
|
|
image: localhost/osint-dashboard:latest
|
|
|
|
|
container_name: osint-camera-scraper
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
profiles: ["ingest"]
|
|
|
|
|
depends_on:
|
|
|
|
|
nats:
|
|
|
|
|
condition: service_started
|
|
|
|
|
db:
|
|
|
|
|
condition: service_healthy
|
|
|
|
|
environment:
|
|
|
|
|
DB_USER: ${DB_USER:-osint}
|
|
|
|
|
DB_PASSWORD: ${DB_PASSWORD:-osint}
|
|
|
|
|
DB_HOST: db
|
|
|
|
|
DB_PORT: ${DB_PORT:-5432}
|
|
|
|
|
DB_NAME: ${DB_NAME:-osint_data}
|
|
|
|
|
NATS_URL: ${NATS_URL:-nats://nats:4222}
|
|
|
|
|
CAMERA_SOURCE_URLS: ${CAMERA_SOURCE_URLS:-}
|
|
|
|
|
CAMERA_SCRAPE_INTERVAL: ${CAMERA_SCRAPE_INTERVAL:-3600}
|
|
|
|
|
CAMERA_REQUEST_DELAY: ${CAMERA_REQUEST_DELAY:-2.0}
|
|
|
|
|
NOMINATIM_URL: ${NOMINATIM_URL:-https://nominatim.openstreetmap.org}
|
|
|
|
|
NOMINATIM_MIN_INTERVAL: ${NOMINATIM_MIN_INTERVAL:-1.1}
|
|
|
|
|
SNAPSHOT_CACHE_DIR: /data/snapshots
|
|
|
|
|
SNAPSHOT_TTL_SECONDS: ${SNAPSHOT_TTL_SECONDS:-300}
|
|
|
|
|
command: ["python", "app/run_camera_service.py"]
|
|
|
|
|
entrypoint: []
|
|
|
|
|
volumes:
|
|
|
|
|
- camera-snapshots:/data/snapshots
|
|
|
|
|
|
2026-08-24 17:28:46 -04:00
|
|
|
# ── News pipeline: hourly scraper (:00) + summarizer (:05) ───────────────
|
|
|
|
|
# Both services point at the EXISTING osint-db (tables articles +
|
|
|
|
|
# article_summaries, created by idempotent alembic migration 003_news).
|
|
|
|
|
# Scheduling replaces the upstream k8s CronJobs with in-compose wall-clock
|
|
|
|
|
# loops (run_news_scraper.py / run_news_summarizer.py).
|
|
|
|
|
news-scraper:
|
|
|
|
|
build:
|
|
|
|
|
context: ./news/scraper
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
platforms: ["linux/arm64"]
|
|
|
|
|
image: localhost/osint-news-scraper:latest
|
|
|
|
|
container_name: osint-news-scraper
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
profiles: ["ingest"]
|
|
|
|
|
depends_on:
|
|
|
|
|
db:
|
|
|
|
|
condition: service_healthy
|
|
|
|
|
environment:
|
|
|
|
|
DB_USER: ${DB_USER:-osint}
|
|
|
|
|
DB_PASSWORD: ${DB_PASSWORD:-osint}
|
|
|
|
|
DB_HOST: db
|
|
|
|
|
DB_PORT: ${DB_PORT:-5432}
|
|
|
|
|
DB_NAME: ${DB_NAME:-osint_data}
|
|
|
|
|
LOG_LEVEL: ${NEWS_LOG_LEVEL:-INFO}
|
|
|
|
|
NEWS_SCRAPE_MINUTE: ${NEWS_SCRAPE_MINUTE:-0}
|
|
|
|
|
NEWS_SCRAPE_RUN_ON_START: ${NEWS_SCRAPE_RUN_ON_START:-1}
|
|
|
|
|
# Override the image ENTRYPOINT ["scrapy"] with the scheduler loop.
|
|
|
|
|
entrypoint: []
|
|
|
|
|
command: ["python", "run_news_scraper.py"]
|
|
|
|
|
|
|
|
|
|
news-summarizer:
|
|
|
|
|
build:
|
|
|
|
|
context: ./news/summerizer
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
platforms: ["linux/arm64"]
|
|
|
|
|
image: localhost/osint-news-summarizer:latest
|
|
|
|
|
container_name: osint-news-summarizer
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
profiles: ["ingest"]
|
|
|
|
|
depends_on:
|
|
|
|
|
db:
|
|
|
|
|
condition: service_healthy
|
|
|
|
|
environment:
|
|
|
|
|
DB_USER: ${DB_USER:-osint}
|
|
|
|
|
DB_PASSWORD: ${DB_PASSWORD:-osint}
|
|
|
|
|
DB_HOST: db
|
|
|
|
|
DB_PORT: ${DB_PORT:-5432}
|
|
|
|
|
DB_NAME: ${DB_NAME:-osint_data}
|
|
|
|
|
# Required to do real work; unset → the loop logs and idles.
|
|
|
|
|
GEMINI_API_KEY: ${GEMINI_API_KEY:-}
|
|
|
|
|
SUMMARY_MODEL: ${SUMMARY_MODEL:-gemini-2.0-flash}
|
|
|
|
|
BATCH_SIZE: ${NEWS_BATCH_SIZE:-50}
|
|
|
|
|
SUMMARY_WINDOW_HOURS: ${SUMMARY_WINDOW_HOURS:-1}
|
|
|
|
|
INCLUDE_FUTURES: ${INCLUDE_FUTURES:-0}
|
|
|
|
|
NEWS_SUMMARIZE_MINUTE: ${NEWS_SUMMARIZE_MINUTE:-5}
|
|
|
|
|
NEWS_SUMMARIZE_RUN_ON_START: ${NEWS_SUMMARIZE_RUN_ON_START:-1}
|
|
|
|
|
command: ["python", "run_news_summarizer.py"]
|
|
|
|
|
|
2026-07-07 17:50:51 -04:00
|
|
|
volumes:
|
|
|
|
|
osint-pgdata:
|
2026-08-24 14:54:51 -04:00
|
|
|
camera-snapshots:
|