mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-09-02 01:18:18 +00:00
822 lines
30 KiB
YAML
822 lines
30 KiB
YAML
name: ${COMPOSE_PROJECT_NAME:-meshcore-analytics}
|
|
|
|
x-anubis-base: &anubis-base
|
|
image: ghcr.io/techarohq/anubis:v1.25.0
|
|
restart: always
|
|
logging: &json-log-limits
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
volumes:
|
|
- ./anubis/botPolicy.yaml:/data/botPolicy.yaml:ro
|
|
|
|
x-internal-hosts: &internal-hosts
|
|
{} # Docker's internal DNS handles service name resolution
|
|
|
|
services:
|
|
timescaledb:
|
|
image: timescale/timescaledb:latest-pg16
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "4g"
|
|
memswap_limit: "4g"
|
|
stop_grace_period: 2m
|
|
command: ["postgres", "-c", "max_connections=300", "-c", "max_parallel_workers_per_gather=0"]
|
|
ports:
|
|
- "${INFRA_BIND_ADDRESS:-127.0.0.1}:5432:5432"
|
|
environment:
|
|
POSTGRES_DB: ${POSTGRES_DB:-meshcore}
|
|
POSTGRES_USER: ${POSTGRES_USER:-meshcore}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD required}
|
|
volumes:
|
|
- timescaledb_data:/var/lib/postgresql/data
|
|
- ./backend/src/db/schema/base.sql:/docker-entrypoint-initdb.d/001_schema.sql:ro
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-meshcore} -d ${POSTGRES_DB:-meshcore}"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 10
|
|
start_period: 30s
|
|
|
|
mosquitto:
|
|
<<: *internal-hosts
|
|
image: eclipse-mosquitto:2
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "128m"
|
|
memswap_limit: "128m"
|
|
ports:
|
|
- "${INFRA_BIND_ADDRESS:-127.0.0.1}:9001:9001"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pgrep mosquitto >/dev/null && nc -z 127.0.0.1 9001"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 5
|
|
start_period: 10s
|
|
volumes:
|
|
- ./mosquitto:/mosquitto/config
|
|
- mosquitto_data:/mosquitto/data
|
|
- mosquitto_log:/mosquitto/log
|
|
|
|
redis:
|
|
<<: *internal-hosts
|
|
image: redis:7-alpine
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "512m"
|
|
memswap_limit: "512m"
|
|
command: redis-server --requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD required}
|
|
environment:
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
|
ports:
|
|
- "${INFRA_BIND_ADDRESS:-127.0.0.1}:6379:6379"
|
|
volumes:
|
|
- redis_data:/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "redis-cli -a $REDIS_PASSWORD --no-auth-warning ping"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 5
|
|
|
|
# Apply additive, versioned migrations once before the application starts.
|
|
# Base schema creation remains in the database init hook for fresh volumes;
|
|
# this service keeps existing volumes current while backend startup stays fast.
|
|
db-migrate:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.backend
|
|
restart: "no"
|
|
logging: *json-log-limits
|
|
mem_limit: "256m"
|
|
memswap_limit: "256m"
|
|
command: ["node", "dist/tools/migrate.js"]
|
|
environment:
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
DATABASE_STATEMENT_TIMEOUT_MS: 0
|
|
NODE_ENV: production
|
|
depends_on:
|
|
timescaledb:
|
|
condition: service_healthy
|
|
|
|
backend:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.backend
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "1g"
|
|
memswap_limit: "1g"
|
|
environment:
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
MQTT_BROKER_URL: ${MQTT_BROKER_URL:-ws://mosquitto:9001}
|
|
MQTT_USERNAME: ${MQTT_USERNAME:-backend}
|
|
MQTT_PASSWORD: ${MQTT_PASSWORD}
|
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD required}
|
|
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required}
|
|
OPENTOPODATA_API: ${OPENTOPODATA_API:-https://api.opentopodata.org}
|
|
PORT: ${PORT:-3000}
|
|
MESHCORE_CHANNEL_SECRETS: ${MESHCORE_CHANNEL_SECRETS:-}
|
|
OWNER_DATABASE_URL: ${OWNER_DATABASE_URL:-postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${OWNER_POSTGRES_DB:-meshcore_owner_auth}}
|
|
OWNER_MQTT_USERNAME_MAP: ${OWNER_MQTT_USERNAME_MAP:-}
|
|
OWNER_AUTHORIZATION_MODE: ${OWNER_AUTHORIZATION_MODE:-shadow}
|
|
OWNER_ACL_MODE: ${OWNER_ACL_MODE:-shadow}
|
|
OWNER_ACL_UNMANAGED_USERS: ${OWNER_ACL_UNMANAGED_USERS:-backend,test,test2}
|
|
OWNER_ACL_ALLOW_EMPTY_USERS: ${OWNER_ACL_ALLOW_EMPTY_USERS:-}
|
|
OWNER_ACL_SHADOW_PATH: /tmp/meshcore-owner-acl.shadow
|
|
MOSQUITTO_ACL_PATH: /mosquitto/config/acl
|
|
MOSQUITTO_CONTAINER_NAME: mosquitto
|
|
OWNER_COOKIE_SECRET: ${OWNER_COOKIE_SECRET:-}
|
|
OPERATOR_SITE_TOKEN: ${OPERATOR_SITE_TOKEN:?OPERATOR_SITE_TOKEN required}
|
|
TRUSTED_PROXY_PEERS: ${TRUSTED_PROXY_PEERS:-172.30.0.10,172.30.0.11,172.30.0.12}
|
|
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:3003,http://localhost:3004,https://ukmesh.com,https://www.ukmesh.com,https://app.ukmesh.com,https://test.ukmesh.com}
|
|
RADIO_BOT_URL: ${RADIO_BOT_URL:-}
|
|
VIEWSHED_ENABLED: ${VIEWSHED_ENABLED:-false}
|
|
COVERAGE_MODEL_VERSION: ${COVERAGE_MODEL_VERSION:-5}
|
|
DATABASE_STATEMENT_TIMEOUT_MS: 300000
|
|
DATABASE_POOL_MAX: ${DATABASE_POOL_MAX:-20}
|
|
STATS_DB_QUERY_CONCURRENCY: ${STATS_DB_QUERY_CONCURRENCY:-2}
|
|
DATABASE_SKIP_SCHEMA_INIT: "true"
|
|
WS_INITIAL_STATE_ENABLED: "1"
|
|
WS_INITIAL_STATE_MAX_BYTES: ${WS_INITIAL_STATE_MAX_BYTES:-16777216}
|
|
MQTT_MAX_PAYLOAD_BYTES: ${MQTT_MAX_PAYLOAD_BYTES:-65536}
|
|
MQTT_INGEST_CONCURRENCY: ${MQTT_INGEST_CONCURRENCY:-8}
|
|
MQTT_INGEST_QUEUE_MAX: ${MQTT_INGEST_QUEUE_MAX:-1000}
|
|
PATH_RESOLVE_WORKERS: ${PATH_RESOLVE_WORKERS:-2}
|
|
PATH_RESOLVE_BACKGROUND_QUEUE_MAX: ${PATH_RESOLVE_BACKGROUND_QUEUE_MAX:-128}
|
|
PATH_RESOLVE_INTERACTIVE_QUEUE_MAX: ${PATH_RESOLVE_INTERACTIVE_QUEUE_MAX:-32}
|
|
PATH_RESOLVE_END_TO_END_TIMEOUT_MS: ${PATH_RESOLVE_END_TO_END_TIMEOUT_MS:-15000}
|
|
PATH_RESOLVE_UNIQUE_INFLIGHT_MAX: ${PATH_RESOLVE_UNIQUE_INFLIGHT_MAX:-34}
|
|
PATH_RESOLVE_OBSERVER_VARIANTS_PER_PACKET_MAX: ${PATH_RESOLVE_OBSERVER_VARIANTS_PER_PACKET_MAX:-4}
|
|
PATH_SINGLE_HISTORY_HOURS: ${PATH_SINGLE_HISTORY_HOURS:-168}
|
|
PATH_SINGLE_MAX_OBSERVERS: ${PATH_SINGLE_MAX_OBSERVERS:-128}
|
|
PATH_MULTI_HISTORY_WINDOW_HOURS: ${PATH_MULTI_HISTORY_WINDOW_HOURS:-168}
|
|
PATH_MULTI_MAX_SCAN_ROWS: ${PATH_MULTI_MAX_SCAN_ROWS:-2048}
|
|
PATH_MULTI_MAX_OBSERVERS: ${PATH_MULTI_MAX_OBSERVERS:-128}
|
|
LAZY_PATH_HISTORY_WINDOW_HOURS: ${LAZY_PATH_HISTORY_WINDOW_HOURS:-168}
|
|
LAZY_PATH_MAX_OBSERVATIONS: ${LAZY_PATH_MAX_OBSERVATIONS:-2048}
|
|
LAZY_PATH_MAX_OBSERVERS: ${LAZY_PATH_MAX_OBSERVERS:-128}
|
|
LAZY_PATH_MAX_UNIQUE_HASHES: ${LAZY_PATH_MAX_UNIQUE_HASHES:-64}
|
|
LAZY_PATH_MAX_CANDIDATE_NODES: ${LAZY_PATH_MAX_CANDIDATE_NODES:-2048}
|
|
SPAM_MESSAGE_MAX_CANDIDATE_ROWS: ${SPAM_MESSAGE_MAX_CANDIDATE_ROWS:-10000}
|
|
SPAM_MESSAGE_MAX_MESSAGES: ${SPAM_MESSAGE_MAX_MESSAGES:-2000}
|
|
SPAM_MESSAGE_MAX_OBSERVERS: ${SPAM_MESSAGE_MAX_OBSERVERS:-32}
|
|
SPAM_MESSAGE_MAX_NORMALIZED_CHARS: ${SPAM_MESSAGE_MAX_NORMALIZED_CHARS:-512}
|
|
SPAM_MESSAGE_MAX_INCIDENTS: ${SPAM_MESSAGE_MAX_INCIDENTS:-100}
|
|
SPAM_MESSAGE_MAX_CANDIDATE_CLUSTERS: ${SPAM_MESSAGE_MAX_CANDIDATE_CLUSTERS:-64}
|
|
SPAM_MESSAGE_MAX_EVIDENCE_MEMBERS: ${SPAM_MESSAGE_MAX_EVIDENCE_MEMBERS:-256}
|
|
SPAM_MESSAGE_ANALYSIS_BUDGET_MS: ${SPAM_MESSAGE_ANALYSIS_BUDGET_MS:-5000}
|
|
SPAM_MESSAGE_DB_TIMEOUT_MS: ${SPAM_MESSAGE_DB_TIMEOUT_MS:-15000}
|
|
STATS_UNIQUE_INFLIGHT_MAX: ${STATS_UNIQUE_INFLIGHT_MAX:-32}
|
|
LINK_QUEUE_V3_PRODUCER_ENABLED: ${LINK_QUEUE_V3_PRODUCER_ENABLED:-1}
|
|
LINK_QUEUE_V3_MAX_JOBS: ${LINK_QUEUE_V3_MAX_JOBS:-5000}
|
|
LINK_QUEUE_V3_MAX_BYTES: ${LINK_QUEUE_V3_MAX_BYTES:-67108864}
|
|
LINK_QUEUE_V3_MAX_PAYLOAD_BYTES: ${LINK_QUEUE_V3_MAX_PAYLOAD_BYTES:-32768}
|
|
PLANNED_COVERAGE_TTL_MS: ${PLANNED_COVERAGE_TTL_MS:-86400000}
|
|
PLANNED_COVERAGE_QUEUE_MAX: ${PLANNED_COVERAGE_QUEUE_MAX:-100}
|
|
PLANNED_COVERAGE_MAX_OUTSTANDING_JOBS: ${PLANNED_COVERAGE_MAX_OUTSTANDING_JOBS:-100}
|
|
PLANNED_COVERAGE_MAX_OUTSTANDING_HANDLES: ${PLANNED_COVERAGE_MAX_OUTSTANDING_HANDLES:-10000}
|
|
PLANNED_COVERAGE_MAX_HANDLES_PER_JOB: ${PLANNED_COVERAGE_MAX_HANDLES_PER_JOB:-256}
|
|
MQTT_AUDIT_MAX_OBSERVATIONS_PER_SCAN: ${MQTT_AUDIT_MAX_OBSERVATIONS_PER_SCAN:-4096}
|
|
MQTT_AUDIT_MAX_NODES_PER_USER: ${MQTT_AUDIT_MAX_NODES_PER_USER:-256}
|
|
MQTT_AUDIT_RETENTION_DAYS: ${MQTT_AUDIT_RETENTION_DAYS:-30}
|
|
LINK_REBUILD_LEASE_MS: ${LINK_REBUILD_LEASE_MS:-120000}
|
|
WS_MAX_PAYLOAD_BYTES: ${WS_MAX_PAYLOAD_BYTES:-65536}
|
|
WS_MAX_QUEUE_BYTES: ${WS_MAX_QUEUE_BYTES:-8388608}
|
|
WS_MAX_BUFFERED_BYTES: ${WS_MAX_BUFFERED_BYTES:-33554432}
|
|
MAINTENANCE_ACTIVE: ${MAINTENANCE_ACTIVE:-0}
|
|
MAINTENANCE_MESSAGE: ${MAINTENANCE_MESSAGE:-}
|
|
VIEWSHED_JOB_COOLDOWN_SECONDS: ${VIEWSHED_JOB_COOLDOWN_SECONDS:-300}
|
|
NODE_ENV: production
|
|
depends_on:
|
|
timescaledb:
|
|
condition: service_healthy
|
|
db-migrate:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
mosquitto:
|
|
condition: service_healthy
|
|
ports:
|
|
- "127.0.0.1:3000:3000"
|
|
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"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 6
|
|
start_period: 30s
|
|
|
|
path-learning-worker:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.backend
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "512m"
|
|
memswap_limit: "512m"
|
|
command: ["node", "dist/workers/path-learning.js"]
|
|
environment:
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD required}
|
|
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required}
|
|
DATABASE_STATEMENT_TIMEOUT_MS: 300000
|
|
DATABASE_SKIP_SCHEMA_INIT: "true"
|
|
PATH_LEARNING_MAX_TRAINING_PACKETS: ${PATH_LEARNING_MAX_TRAINING_PACKETS:-120000}
|
|
PATH_LEARNING_MAX_NODES: ${PATH_LEARNING_MAX_NODES:-50000}
|
|
PATH_LEARNING_MAX_LINKS: ${PATH_LEARNING_MAX_LINKS:-200000}
|
|
PATH_LEARNING_MAX_AGGREGATE_KEYS: ${PATH_LEARNING_MAX_AGGREGATE_KEYS:-1000000}
|
|
PATH_LEARNING_RUN_DEADLINE_MS: ${PATH_LEARNING_RUN_DEADLINE_MS:-2700000}
|
|
NODE_ENV: production
|
|
depends_on:
|
|
timescaledb:
|
|
condition: service_healthy
|
|
db-migrate:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
path-history-worker:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.backend
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "512m"
|
|
memswap_limit: "512m"
|
|
command: ["node", "dist/workers/path-history.js"]
|
|
environment:
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
DATABASE_APPLICATION_NAME: meshcore-path-history
|
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD required}
|
|
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required}
|
|
DATABASE_STATEMENT_TIMEOUT_MS: 300000
|
|
DATABASE_SKIP_SCHEMA_INIT: "true"
|
|
PATH_HISTORY_RUN_DEADLINE_MS: ${PATH_HISTORY_RUN_DEADLINE_MS:-7200000}
|
|
PATH_HISTORY_CONCURRENCY: ${PATH_HISTORY_CONCURRENCY:-2}
|
|
PATH_HISTORY_SEGMENT_COUNTER_CAPACITY: ${PATH_HISTORY_SEGMENT_COUNTER_CAPACITY:-65536}
|
|
PATH_MULTI_HISTORY_WINDOW_HOURS: ${PATH_MULTI_HISTORY_WINDOW_HOURS:-168}
|
|
PATH_MULTI_MAX_SCAN_ROWS: ${PATH_MULTI_MAX_SCAN_ROWS:-2048}
|
|
PATH_MULTI_MAX_OBSERVERS: ${PATH_MULTI_MAX_OBSERVERS:-128}
|
|
NODE_ENV: production
|
|
depends_on:
|
|
timescaledb:
|
|
condition: service_healthy
|
|
db-migrate:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: "1.0"
|
|
memory: 512M
|
|
|
|
health-worker:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.backend
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "512m"
|
|
memswap_limit: "512m"
|
|
command: ["node", "dist/workers/health.js"]
|
|
environment:
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD required}
|
|
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:
|
|
condition: service_healthy
|
|
db-migrate:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
synthetic-monitor:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.backend
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "256m"
|
|
memswap_limit: "256m"
|
|
command: ["node", "dist/workers/synthetic-monitor.js"]
|
|
environment:
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
DATABASE_SKIP_SCHEMA_INIT: "true"
|
|
SYNTHETIC_BASE_URL: http://backend:3000
|
|
SYNTHETIC_WS_URL: ws://backend:3000
|
|
SYNTHETIC_INTERVAL_MS: ${SYNTHETIC_INTERVAL_MS:-60000}
|
|
SYNTHETIC_TIMEOUT_MS: ${SYNTHETIC_TIMEOUT_MS:-10000}
|
|
SYNTHETIC_FAILURE_THRESHOLD: ${SYNTHETIC_FAILURE_THRESHOLD:-3}
|
|
ALERT_WEBHOOK_URL: ${ALERT_WEBHOOK_URL:-}
|
|
NODE_ENV: production
|
|
depends_on:
|
|
db-migrate:
|
|
condition: service_completed_successfully
|
|
backend:
|
|
condition: service_healthy
|
|
|
|
link-backfill-worker:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.backend
|
|
restart: on-failure:3
|
|
logging: *json-log-limits
|
|
mem_limit: "512m"
|
|
memswap_limit: "512m"
|
|
command: ["node", "dist/workers/link-backfill.js"]
|
|
environment:
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
MQTT_BROKER_URL: ${MQTT_BROKER_URL:-ws://mosquitto:9001}
|
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD required}
|
|
MESHCORE_CHANNEL_SECRETS: ${MESHCORE_CHANNEL_SECRETS:-}
|
|
DATABASE_SKIP_SCHEMA_INIT: "true"
|
|
NODE_ENV: production
|
|
depends_on:
|
|
timescaledb:
|
|
condition: service_healthy
|
|
db-migrate:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
backend:
|
|
condition: service_healthy
|
|
link-worker:
|
|
condition: service_healthy
|
|
|
|
viewshed-worker:
|
|
<<: *internal-hosts
|
|
profiles: [viewshed]
|
|
build:
|
|
context: ./viewshed-worker
|
|
dockerfile: Dockerfile
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "5g"
|
|
memswap_limit: "5g"
|
|
environment:
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD required}
|
|
SRTM_DIR: /data/srtm
|
|
WORKER_MODE: viewshed
|
|
VIEWSHED_ENABLED: ${VIEWSHED_ENABLED:-false}
|
|
NUM_WORKERS: '1'
|
|
COVERAGE_MODEL: ${COVERAGE_MODEL:-rf_radial_100m}
|
|
COVERAGE_MODEL_VERSION: ${COVERAGE_MODEL_VERSION:-5}
|
|
PLANNED_COVERAGE_TTL_SECONDS: ${PLANNED_COVERAGE_TTL_SECONDS:-86400}
|
|
PLANNED_COVERAGE_QUEUE_MAX: ${PLANNED_COVERAGE_QUEUE_MAX:-100}
|
|
volumes:
|
|
- srtm_data:/data/srtm
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "python3 -c \"import os,redis; r=redis.Redis.from_url(os.environ['REDIS_URL'],password=os.environ.get('REDIS_PASSWORD')); raise SystemExit(0 if r.exists('meshcore:viewshed:worker_heartbeat') else 1)\""]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 20s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '1.5'
|
|
memory: '5g'
|
|
depends_on:
|
|
timescaledb:
|
|
condition: service_healthy
|
|
db-migrate:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
link-worker:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: ./viewshed-worker
|
|
dockerfile: Dockerfile
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "2g"
|
|
memswap_limit: "2g"
|
|
cpus: 2.0
|
|
environment:
|
|
DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD required}
|
|
RADIO_BOT_URL: ${LINK_WORKER_RADIO_BOT_URL:-}
|
|
SRTM_DIR: /data/srtm
|
|
WORKER_MODE: link
|
|
NUM_WORKERS: '1'
|
|
LINK_QUEUE_V3_MAX_JOBS: ${LINK_QUEUE_V3_MAX_JOBS:-5000}
|
|
LINK_QUEUE_V3_MAX_BYTES: ${LINK_QUEUE_V3_MAX_BYTES:-67108864}
|
|
LINK_QUEUE_V3_MAX_PAYLOAD_BYTES: ${LINK_QUEUE_V3_MAX_PAYLOAD_BYTES:-32768}
|
|
LINK_QUEUE_V3_MAX_ATTEMPTS: ${LINK_QUEUE_V3_MAX_ATTEMPTS:-5}
|
|
LINK_QUEUE_V3_LEASE_MS: ${LINK_QUEUE_V3_LEASE_MS:-120000}
|
|
volumes:
|
|
- srtm_data:/data/srtm
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "python3 -c \"import os,redis; r=redis.Redis.from_url(os.environ['REDIS_URL'],password=os.environ.get('REDIS_PASSWORD')); raise SystemExit(0 if r.exists('meshcore:link:v3:worker_heartbeat') else 1)\""]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 20s
|
|
depends_on:
|
|
timescaledb:
|
|
condition: service_healthy
|
|
db-migrate:
|
|
condition: service_completed_successfully
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
# ml-path-learner:
|
|
# build:
|
|
# context: ./ml-path-learner
|
|
# dockerfile: Dockerfile
|
|
# restart: always
|
|
# logging: *json-log-limits
|
|
# cpus: 2.0
|
|
# mem_limit: '2g'
|
|
# environment:
|
|
# DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}
|
|
# GOLD_EXTRACTION_INTERVAL_MINS: '15'
|
|
# TRAINING_INTERVAL_MINS: '30'
|
|
# MIN_GOLD_PATHS: '100'
|
|
# CONFIDENCE_THRESHOLD: '0.85'
|
|
# ML_RETAIN_VARIANT_RESULT_GENERATIONS: ${ML_RETAIN_VARIANT_RESULT_GENERATIONS:-96}
|
|
# ML_RETAIN_MODEL_ARTIFACT_GENERATIONS: ${ML_RETAIN_MODEL_ARTIFACT_GENERATIONS:-96}
|
|
# ML_CLEANUP_GENERATION_BATCH_SIZE: ${ML_CLEANUP_GENERATION_BATCH_SIZE:-24}
|
|
# depends_on:
|
|
# timescaledb:
|
|
# condition: service_healthy
|
|
|
|
app-ukmesh:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.app
|
|
args:
|
|
VITE_APP_HOSTNAME: app.ukmesh.com
|
|
VITE_NETWORK: ukmesh
|
|
VITE_SITE: ukmesh
|
|
VITE_VIEWSHED_ENABLED: ${VIEWSHED_ENABLED:-false}
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "128m"
|
|
memswap_limit: "128m"
|
|
depends_on:
|
|
backend:
|
|
condition: service_healthy
|
|
ports:
|
|
- "127.0.0.1:3003:80"
|
|
networks:
|
|
default:
|
|
ipv4_address: 172.30.0.10
|
|
volumes:
|
|
- ${TERRAIN_TILES_PATH:-./terrain-tiles}:/terrain-tiles:ro
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null && wget -qO- http://127.0.0.1/readyz >/dev/null"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 6
|
|
start_period: 15s
|
|
|
|
website-ukmesh:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.website
|
|
args:
|
|
VITE_APP_HOSTNAME: app.ukmesh.com
|
|
VITE_SITE: ukmesh
|
|
VITE_VIEWSHED_ENABLED: ${VIEWSHED_ENABLED:-false}
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "128m"
|
|
memswap_limit: "128m"
|
|
ports:
|
|
- "127.0.0.1:3004:80"
|
|
networks:
|
|
default:
|
|
ipv4_address: 172.30.0.11
|
|
depends_on:
|
|
backend:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null && wget -qO- http://127.0.0.1/readyz >/dev/null"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 6
|
|
start_period: 15s
|
|
|
|
mesh-health-check:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.mesh-health-check
|
|
args:
|
|
MESH_HEALTH_CHECK_REF: ${HEALTHCHECK_SOURCE_REF:-main}
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "512m"
|
|
memswap_limit: "512m"
|
|
environment:
|
|
PORT: "3090"
|
|
APP_TITLE: ${HEALTHCHECK_APP_TITLE:-UKMesh Health Check}
|
|
APP_EYEBROW: ${HEALTHCHECK_APP_EYEBROW:-MeshCore Observer Coverage}
|
|
APP_HEADLINE: ${HEALTHCHECK_APP_HEADLINE:-Check your UK mesh reach.}
|
|
APP_DESCRIPTION: ${HEALTHCHECK_APP_DESCRIPTION:-Generate a test code, send it to the configured channel, and watch observer coverage build in real time.}
|
|
EXTERNAL_LINK_URL: ${HEALTHCHECK_EXTERNAL_LINK_URL:-https://ukmesh.com}
|
|
EXTERNAL_LINK_LABEL: ${HEALTHCHECK_EXTERNAL_LINK_LABEL:-UKMesh}
|
|
LOG_LEVEL: ${HEALTHCHECK_LOG_LEVEL:-info}
|
|
TRUST_PROXY: "1"
|
|
MQTT_HOST: mosquitto
|
|
MQTT_PORT: "9001"
|
|
MQTT_USERNAME: ${MQTT_USERNAME:-backend}
|
|
MQTT_PASSWORD: ${MQTT_PASSWORD}
|
|
MQTT_TOPIC: ${HEALTHCHECK_MQTT_TOPIC:-meshcore/+/+/packets,meshcore/+/+/status,meshcore/+/+/internal}
|
|
MQTT_TRANSPORT: websockets
|
|
MQTT_WS_PATH: /
|
|
MQTT_TLS: "false"
|
|
DASH_BROKER_HOST: ${HEALTHCHECK_DASH_BROKER_HOST:-mqtt.ukmesh.com}
|
|
TEST_CHANNEL_NAME: ${HEALTHCHECK_TEST_CHANNEL_NAME:-ukmeshtest}
|
|
TEST_CHANNEL_SECRET: ${HEALTHCHECK_TEST_CHANNEL_SECRET:-}
|
|
TEST_CHANNEL_SECRET_SOURCE_NAME: ${HEALTHCHECK_TEST_CHANNEL_SECRET_SOURCE_NAME:-test}
|
|
TEST_CHANNEL_HASH: ${HEALTHCHECK_TEST_CHANNEL_HASH:-}
|
|
MESHCORE_CHANNEL_SECRETS: ${MESHCORE_CHANNEL_SECRETS:-}
|
|
SESSION_TTL_SECONDS: ${HEALTHCHECK_SESSION_TTL_SECONDS:-600}
|
|
RESULT_RETENTION_SECONDS: ${HEALTHCHECK_RESULT_RETENTION_SECONDS:-604800}
|
|
MAX_USES_PER_CODE: ${HEALTHCHECK_MAX_USES_PER_CODE:-3}
|
|
KNOWN_OBSERVERS: ${HEALTHCHECK_KNOWN_OBSERVERS:-}
|
|
OBSERVER_ACTIVE_WINDOW_SECONDS: ${HEALTHCHECK_OBSERVER_ACTIVE_WINDOW_SECONDS:-900}
|
|
OBSERVER_RETENTION_SECONDS: ${HEALTHCHECK_OBSERVER_RETENTION_SECONDS:-14400}
|
|
OBSERVERS_FILE: /app/data/observer.json
|
|
RESULTS_FILE: /app/data/session-results.json
|
|
REGIONS_FILE: ${HEALTHCHECK_REGIONS_FILE:-regions/uk.geojson}
|
|
TURNSTILE_ENABLED: ${HEALTHCHECK_TURNSTILE_ENABLED:-false}
|
|
TURNSTILE_SITE_KEY: ${HEALTHCHECK_TURNSTILE_SITE_KEY:-}
|
|
TURNSTILE_SECRET_KEY: ${HEALTHCHECK_TURNSTILE_SECRET_KEY:-}
|
|
depends_on:
|
|
mosquitto:
|
|
condition: service_healthy
|
|
ports:
|
|
- "127.0.0.1:3090:3090"
|
|
volumes:
|
|
- mesh_health_check_data:/app/data
|
|
- ./healthcheck-overrides/index.html:/app/public/index.html:ro
|
|
- ./healthcheck-overrides/share.html:/app/public/share.html:ro
|
|
- ./healthcheck-overrides/sw.js:/app/public/sw.js:ro
|
|
- ./healthcheck-overrides/ukmesh-theme.css:/app/public/ukmesh-theme.css:ro
|
|
healthcheck:
|
|
# The upstream app has no /healthz route; its SPA fallback returns 200
|
|
# there even when the API is unavailable. Probe a real JSON endpoint.
|
|
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3090/api/bootstrap').then(r=>process.exit(r.ok && (r.headers.get('content-type') || '').includes('application/json') ? 0 : 1)).catch(()=>process.exit(1))\""]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 6
|
|
start_period: 30s
|
|
|
|
website-dev:
|
|
<<: *internal-hosts
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.website
|
|
args:
|
|
VITE_APP_HOSTNAME: ${VITE_DEV_APP_HOSTNAME:-app-dev.ukmesh.com}
|
|
VITE_NETWORK: test
|
|
VITE_SITE: dev
|
|
VITE_OBSERVER_ID: ${VITE_DEV_OBSERVER_ID:-}
|
|
VITE_SITE_DISPLAY_NAME: ${VITE_DEV_SITE_DISPLAY_NAME:-UK Mesh Test}
|
|
VITE_SITE_FOOTER_NAME: ${VITE_DEV_SITE_FOOTER_NAME:-UK Mesh Test}
|
|
VITE_SITE_APP_URL: ${VITE_DEV_SITE_APP_URL:-https://test.ukmesh.com}
|
|
VITE_SITE_HOME_URL: ${VITE_DEV_SITE_HOME_URL:-https://test.ukmesh.com}
|
|
VITE_VIEWSHED_ENABLED: ${VIEWSHED_ENABLED:-false}
|
|
restart: always
|
|
profiles: [dev]
|
|
logging: *json-log-limits
|
|
mem_limit: "128m"
|
|
memswap_limit: "128m"
|
|
ports:
|
|
- "127.0.0.1:3006:80"
|
|
networks:
|
|
default:
|
|
ipv4_address: 172.30.0.12
|
|
depends_on:
|
|
backend:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 6
|
|
start_period: 15s
|
|
|
|
# ── Anubis bot protection ─────────────────────────────────────────────────────
|
|
# One sidecar per public HTTP service. Cloudflare Tunnel routes to these
|
|
# containers (port 8923) instead of the origin services directly.
|
|
# MQTT (mosquitto:9001) is intentionally excluded — non-browser clients.
|
|
# Dev services are intentionally excluded — low value.
|
|
|
|
anubis-website-ukmesh:
|
|
<<: [*anubis-base, *internal-hosts]
|
|
mem_limit: "128m"
|
|
memswap_limit: "128m"
|
|
environment:
|
|
ED25519_PRIVATE_KEY_HEX: "${ANUBIS_ED25519_PRIVATE_KEY_HEX:?ANUBIS_ED25519_PRIVATE_KEY_HEX required}"
|
|
BIND: ":8923"
|
|
TARGET: "http://website-ukmesh:80"
|
|
COOKIE_DOMAIN: "ukmesh.com"
|
|
COOKIE_SECURE: "true"
|
|
COOKIE_SAME_SITE: "Lax"
|
|
COOKIE_EXPIRATION_TIME: "168h"
|
|
POLICY_FNAME: "/data/botPolicy.yaml"
|
|
SLOG_LEVEL: "INFO"
|
|
CUSTOM_REAL_IP_HEADER: "CF-Connecting-IP"
|
|
depends_on:
|
|
website-ukmesh:
|
|
condition: service_healthy
|
|
networks:
|
|
default:
|
|
ipv4_address: 172.30.0.20
|
|
|
|
anubis-app-ukmesh:
|
|
<<: [*anubis-base, *internal-hosts]
|
|
mem_limit: "128m"
|
|
memswap_limit: "128m"
|
|
environment:
|
|
ED25519_PRIVATE_KEY_HEX: "${ANUBIS_ED25519_PRIVATE_KEY_HEX:?ANUBIS_ED25519_PRIVATE_KEY_HEX required}"
|
|
BIND: ":8923"
|
|
TARGET: "http://app-ukmesh:80"
|
|
COOKIE_DOMAIN: "ukmesh.com"
|
|
COOKIE_SECURE: "true"
|
|
COOKIE_SAME_SITE: "Lax"
|
|
COOKIE_EXPIRATION_TIME: "168h"
|
|
POLICY_FNAME: "/data/botPolicy.yaml"
|
|
SLOG_LEVEL: "INFO"
|
|
CUSTOM_REAL_IP_HEADER: "CF-Connecting-IP"
|
|
depends_on:
|
|
app-ukmesh:
|
|
condition: service_healthy
|
|
networks:
|
|
default:
|
|
ipv4_address: 172.30.0.21
|
|
|
|
anubis-health-check:
|
|
<<: [*anubis-base, *internal-hosts]
|
|
mem_limit: "128m"
|
|
memswap_limit: "128m"
|
|
environment:
|
|
ED25519_PRIVATE_KEY_HEX: "${ANUBIS_ED25519_PRIVATE_KEY_HEX:?ANUBIS_ED25519_PRIVATE_KEY_HEX required}"
|
|
BIND: ":8923"
|
|
TARGET: "http://mesh-health-check:3090"
|
|
COOKIE_DOMAIN: "ukmesh.com"
|
|
COOKIE_SECURE: "true"
|
|
COOKIE_SAME_SITE: "Lax"
|
|
COOKIE_EXPIRATION_TIME: "168h"
|
|
POLICY_FNAME: "/data/botPolicy.yaml"
|
|
SLOG_LEVEL: "INFO"
|
|
CUSTOM_REAL_IP_HEADER: "CF-Connecting-IP"
|
|
depends_on:
|
|
mesh-health-check:
|
|
condition: service_healthy
|
|
networks:
|
|
default:
|
|
ipv4_address: 172.30.0.22
|
|
# Cloudflare Tunnel currently routes healthcheck.ukmesh.com to
|
|
# http://anubis-mesh-health-check:8923. Keep this alias unless the
|
|
# tunnel ingress config is changed at Cloudflare too.
|
|
aliases:
|
|
- anubis-mesh-health-check
|
|
|
|
cloudflared:
|
|
image: cloudflare/cloudflared:latest
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "256m"
|
|
memswap_limit: "256m"
|
|
command: tunnel run
|
|
profiles: [tunnel]
|
|
networks:
|
|
default:
|
|
ipv4_address: 172.30.0.30
|
|
meshcore-beacon-ukmesh: {}
|
|
environment:
|
|
TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN}
|
|
depends_on:
|
|
backend:
|
|
condition: service_healthy
|
|
anubis-website-ukmesh:
|
|
condition: service_started
|
|
anubis-app-ukmesh:
|
|
condition: service_started
|
|
anubis-health-check:
|
|
condition: service_started
|
|
|
|
loki:
|
|
<<: *internal-hosts
|
|
image: grafana/loki:3.4
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "512m"
|
|
memswap_limit: "512m"
|
|
command: -config.file=/etc/loki/loki.yaml
|
|
volumes:
|
|
- ./logging/loki.yaml:/etc/loki/loki.yaml:ro
|
|
- loki_data:/loki
|
|
|
|
promtail:
|
|
<<: *internal-hosts
|
|
image: grafana/promtail:3.4
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "256m"
|
|
memswap_limit: "256m"
|
|
command: -config.file=/etc/promtail/promtail.yaml
|
|
volumes:
|
|
- ./logging/promtail.yaml:/etc/promtail/promtail.yaml:ro
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
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
|
|
restart: always
|
|
logging: *json-log-limits
|
|
mem_limit: "512m"
|
|
memswap_limit: "512m"
|
|
environment:
|
|
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD required}
|
|
GF_USERS_ALLOW_SIGN_UP: "false"
|
|
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
|
volumes:
|
|
- ./logging/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
- ./logging/grafana/dashboards:/etc/grafana/dashboards:ro
|
|
- grafana_data:/var/lib/grafana
|
|
ports:
|
|
- "127.0.0.1:3010:3000"
|
|
depends_on:
|
|
- loki
|
|
- prometheus
|
|
|
|
networks:
|
|
default:
|
|
ipam:
|
|
config:
|
|
# Fixed proxy addresses make the backend's immediate-peer trust exact.
|
|
- subnet: 172.30.0.0/24
|
|
meshcore-beacon-ukmesh:
|
|
external: true
|
|
name: meshcore-beacon-ukmesh_default
|
|
|
|
volumes:
|
|
timescaledb_data:
|
|
mosquitto_data:
|
|
mosquitto_log:
|
|
redis_data:
|
|
srtm_data:
|
|
loki_data:
|
|
grafana_data:
|
|
prometheus_data:
|
|
mesh_health_check_data:
|