worldmonitor Docker & Self-Hosted Deployment

GitHub

Real-time global intelligence dashboard. AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational...

81,304 stars TypeScript TypeScript 1 file ยท ~6,789 tokens #agent#ai#dashboard#geopolitics#mcp#mcp-server#monitoring#news
RAW Compose
1. Pull Image (DOCKERHUB) docker pull library/worldmonitor
2. Docker Run (1-Click Launch) docker run -d --name worldmonitor -p 3000:3000 --restart always worldmonitor:latest
3. Podman Run podman run -d --name worldmonitor -p 3000:3000 --restart always worldmonitor:latest
4. Skopeo Inspect (Zero-Pull Manifest) skopeo inspect docker://docker.io/library/worldmonitor:latest
5. ORAS Pull (OCI Artifact) oras pull worldmonitor:latest
6. Compose Deployment docker compose up -d
docker-compose.yml
Download .yml
# =============================================================================
# World Monitor โ€” Docker / Podman Compose
# =============================================================================
# Self-contained stack: app + Redis + AIS relay.
#
# Quick start:
#   cp .env.example .env        # add your API keys
#   docker compose up -d --build
#
# The app will be available at http://localhost:3000
# =============================================================================

services:

  worldmonitor:
    build:
      context: .
      dockerfile: Dockerfile
    image: worldmonitor:latest
    container_name: worldmonitor
    ports:
      - "${WM_PORT:-3000}:8080"
    environment:
      UPSTASH_REDIS_REST_URL: "http://redis-rest:80"
      UPSTASH_REDIS_REST_TOKEN: "${REDIS_TOKEN:?REDIS_TOKEN required โ€” generate with: openssl rand -hex 32}"
      WM_SESSION_SECRET: "${WM_SESSION_SECRET:?WM_SESSION_SECRET required โ€” generate with: openssl rand -hex 32}"
      LOCAL_API_PORT: "46123"
      LOCAL_API_MODE: "docker"
      LOCAL_API_CLOUD_FALLBACK: "false"
      WM_TRUSTED_PROXY_CIDRS: "${WM_TRUSTED_PROXY_CIDRS:-}"
      WS_RELAY_URL: "http://ais-relay:3004"
      # Operator keys accepted by MCP and REST through X-WorldMonitor-Key.
      WORLDMONITOR_VALID_KEYS: "${WORLDMONITOR_VALID_KEYS:-}"
      RELAY_SHARED_SECRET: "${RELAY_SHARED_SECRET:?RELAY_SHARED_SECRET required โ€” generate with: openssl rand -hex 32}"
      WORLDMONITOR_RELAY_KEY: "${WORLDMONITOR_RELAY_KEY:-}"
      # LLM provider (any OpenAI-compatible endpoint)
      LLM_API_URL: "${LLM_API_URL:-}"
      LLM_API_KEY: "${LLM_API_KEY:-}"
      LLM_MODEL: "${LLM_MODEL:-}"
      OPENROUTER_API_KEY: "${OPENROUTER_API_KEY:-}"
      # Data source API keys (optional โ€” features degrade gracefully)
      AISSTREAM_API_KEY: "${AISSTREAM_API_KEY:-}"
      FINNHUB_API_KEY: "${FINNHUB_API_KEY:-}"
      EIA_API_KEY: "${EIA_API_KEY:-}"
      FRED_API_KEY: "${FRED_API_KEY:-}"
      ACLED_EMAIL: "${ACLED_EMAIL:-}"
      ACLED_PASSWORD: "${ACLED_PASSWORD:-}"
      ACLED_ACCESS_TOKEN: "${ACLED_ACCESS_TOKEN:-}"
      NASA_FIRMS_API_KEY: "${NASA_FIRMS_API_KEY:-}"
      CLOUDFLARE_API_TOKEN: "${CLOUDFLARE_API_TOKEN:-}"
      AVIATIONSTACK_API: "${AVIATIONSTACK_API:-}"
      TRAVELPAYOUTS_API_TOKEN: "${TRAVELPAYOUTS_API_TOKEN:-}"
    # Docker secrets (recommended for API keys โ€” keeps them out of docker inspect).
    # Create secrets/ dir with one file per key, then uncomment below.
    # See SELF_HOSTING.md or docker-compose.override.yml for details.
    # secrets:
    #   - OPENROUTER_API_KEY
    #   - AISSTREAM_API_KEY
    #   - FINNHUB_API_KEY
    #   - FRED_API_KEY
    #   - NASA_FIRMS_API_KEY
    #   - LLM_API_KEY
    depends_on:
      redis-rest:
        condition: service_started
      ais-relay:
        condition: service_started
    restart: unless-stopped

  ais-relay:
    build:
      context: .
      dockerfile: Dockerfile.relay
    image: worldmonitor-ais-relay:latest
    container_name: worldmonitor-ais-relay
    environment:
      RELAY_SHARED_SECRET: "${RELAY_SHARED_SECRET:?RELAY_SHARED_SECRET required โ€” generate with: openssl rand -hex 32}"
      AISSTREAM_API_KEY: "${AISSTREAM_API_KEY:-}"
      FINNHUB_API_KEY: "${FINNHUB_API_KEY:-}"
      MARKET_YAHOO_REFRESH_INTERVAL_MS: "${MARKET_YAHOO_REFRESH_INTERVAL_MS:-900000}"
      UPSTASH_REDIS_REST_URL: "http://redis-rest:80"
      UPSTASH_REDIS_REST_TOKEN: "${REDIS_TOKEN:?REDIS_TOKEN required โ€” generate with: openssl rand -hex 32}"
      UPSTASH_ALLOW_INSECURE_HTTP: "true"
      UCDP_ACCESS_TOKEN: "${UCDP_ACCESS_TOKEN:-}"
      # Optional: Jev shadows the classify seed and records disagreements; it decides nothing.
      TYPESAFE_API_KEY: "${TYPESAFE_API_KEY:-}"
      TYPESAFE_BASE_URL: "${TYPESAFE_BASE_URL:-}"
      # Classify reads list-feed-digest from this install, not production (#7437).
      # Do not depends_on worldmonitor here โ€” the app already depends_on ais-relay.
      # Transient connection retries cover the compose boot race instead.
      API_BASE_URL: "${API_BASE_URL:-http://worldmonitor:8080}"
      WORLDMONITOR_RELAY_KEY: "${WORLDMONITOR_RELAY_KEY:-}"
      PORT: "3004"
    depends_on:
      redis-rest:
        condition: service_started
    restart: unless-stopped

  redis:
    image: docker.io/redis:7-alpine
    container_name: worldmonitor-redis
    # --requirepass closes off the network so any future sidecar / compromised
    # dependency added to the worldmonitor network can't read or write cache
    # entries without the password. Defense-in-depth alongside the REST token.
    command: >
      redis-server
      --requirepass "${REDIS_PASSWORD:?REDIS_PASSWORD required โ€” generate with: openssl rand -hex 32}"
      --maxmemory 256mb
      --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    restart: unless-stopped

  redis-rest:
    build:
      context: docker
      dockerfile: Dockerfile.redis-rest
    image: worldmonitor-redis-rest:latest
    container_name: worldmonitor-redis-rest
    # The proxy buffers each accepted request body in full (SRH_MAX_BODY_BYTES,
    # 16MB default), so concurrent large publishes multiply. Measured: 64
    # concurrent 16MB POSTs reached ~933MB RSS. Buffers are off-heap, so a Node
    # heap flag would not contain them โ€” bound it at the container, the same way
    # the redis service is bounded by --maxmemory. 512m leaves ~7x headroom over
    # one max-size publish (16MB body + concat + UTF-16 string โ‰ˆ 64MB transient).
    mem_limit: 512m
    ports:
      - "127.0.0.1:8079:80"
    environment:
      SRH_TOKEN: "${REDIS_TOKEN:?REDIS_TOKEN required โ€” generate with: openssl rand -hex 32}"
      # Defense-in-depth: the redis service's --requirepass already fails
      # compose validation if REDIS_PASSWORD is unset, but the explicit
      # :? here protects against a future PR removing --requirepass or
      # copying this connection string into a new service.
      SRH_CONNECTION_STRING: "redis://:${REDIS_PASSWORD:?REDIS_PASSWORD required โ€” generate with: openssl rand -hex 32}@redis:6379"
    depends_on:
      - redis
    restart: unless-stopped

# Docker secrets โ€” uncomment and point to your secret files.
# Example: echo "sk-or-abc123" > secrets/openrouter_api_key.txt
# secrets:
#   OPENROUTER_API_KEY:
#     file: ./secrets/openrouter_api_key.txt
#   AISSTREAM_API_KEY:
#     file: ./secrets/aisstream_api_key.txt
#   FINNHUB_API_KEY:
#     file: ./secrets/finnhub_api_key.txt
#   FRED_API_KEY:
#     file: ./secrets/fred_api_key.txt
#   NASA_FIRMS_API_KEY:
#     file: ./secrets/nasa_firms_api_key.txt
#   LLM_API_KEY:
#     file: ./secrets/llm_api_key.txt

volumes:
  redis-data: