Files
ukmesh/docker-compose.yml
T

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: