Files
ukmesh/docker-compose.yml
T

1082 lines
41 KiB
YAML

name: ${COMPOSE_PROJECT_NAME:-meshcore-analytics}
x-anubis-base: &anubis-base
image: ghcr.io/techarohq/anubis@sha256:5cf4c0d6c7ea01a356c6685a779971bdd28d6a5a4a6719201f73a9c4ae108db0
restart: always
user: "1000:1000"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m,mode=1777
logging: &json-log-limits
driver: journald
options:
tag: "{{.Name}}"
labels: "com.docker.compose.service,com.docker.compose.project"
volumes:
- ./anubis/botPolicy.yaml:/data/botPolicy.yaml:ro
x-internal-hosts: &internal-hosts
{} # Docker's internal DNS handles service name resolution
x-application-hardening: &application-hardening
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m,mode=1777
services:
# 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, *application-hardening]
image: ${BACKEND_IMAGE:-meshcore-analytics-backend:local}
build:
context: .
dockerfile: Dockerfile.backend
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
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
backend:
<<: *internal-hosts
image: ${BACKEND_IMAGE:-meshcore-analytics-backend:local}
build:
context: .
dockerfile: Dockerfile.backend
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
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}
DATABASE_APPLICATION_NAME: meshcore-backend
OWNER_DATABASE_APPLICATION_NAME: meshcore-backend-owner-auth
OWNER_DATABASE_ADMIN_APPLICATION_NAME: meshcore-backend-owner-auth-admin
MQTT_BROKER_URL: ${MQTT_BROKER_URL:-ws://mosquitto:9001}
MQTT_USERNAME: ${MQTT_USERNAME:-backend}
MQTT_PASSWORD: ${MQTT_PASSWORD}
MQTT_CLIENT_ID: ${MQTT_CLIENT_ID:-meshcore-analytics-ingest}
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD required}
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required}
PORT: ${PORT:-3000}
METRICS_PORT: ${METRICS_PORT:-9091}
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
OWNER_ACL_RELOAD_URL: http://mosquitto-reloader:8080/reload
OWNER_ACL_RELOAD_TOKEN: ${JWT_SECRET:?JWT_SECRET required}
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}
API_RATE_LIMIT_MAX: ${API_RATE_LIMIT_MAX:-120}
RADIO_BOT_URL: ${RADIO_BOT_URL:-}
PUBLIC_FEATURE_INFERRED_NODES_ENABLED: ${PUBLIC_FEATURE_INFERRED_NODES_ENABLED:-true}
PUBLIC_FEATURE_PACKET_ARCS_ENABLED: ${PUBLIC_FEATURE_PACKET_ARCS_ENABLED:-true}
PUBLIC_FEATURE_HEATMAP_ENABLED: ${PUBLIC_FEATURE_HEATMAP_ENABLED:-false}
PUBLIC_FEATURE_CONFIG_TTL_SECONDS: ${PUBLIC_FEATURE_CONFIG_TTL_SECONDS:-30}
# 15 min: the ukmesh chart snapshot regen runs multibyte analytics that
# exceed 5 min on the 2vCPU VPS (was 300000; raised 2026-08-08)
DATABASE_STATEMENT_TIMEOUT_MS: 900000
DATABASE_POOL_MAX: ${DATABASE_POOL_MAX:-20}
STATS_DB_QUERY_CONCURRENCY: ${STATS_DB_QUERY_CONCURRENCY:-2}
STATS_AGGREGATE_READS_ENABLED: ${STATS_AGGREGATE_READS_ENABLED:-false}
STATS_AGGREGATE_SHADOW_ENABLED: ${STATS_AGGREGATE_SHADOW_ENABLED:-false}
STATS_MULTIBYTE_FACTS_READS_ENABLED: ${STATS_MULTIBYTE_FACTS_READS_ENABLED:-false}
STATS_MULTIBYTE_FACTS_SHADOW_ENABLED: ${STATS_MULTIBYTE_FACTS_SHADOW_ENABLED:-false}
WARMUP_NETWORKS: ${WARMUP_NETWORKS:-ukmesh}
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}
# The 30s lifecycle deadline reserves 25s for MQTT disconnect and queue drain.
MQTT_INGEST_QUEUE_MAX: ${MQTT_INGEST_QUEUE_MAX:-20000}
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}
LINK_QUEUE_V3_DEAD_MAX_JOBS: ${LINK_QUEUE_V3_DEAD_MAX_JOBS:-1000}
LINK_QUEUE_V3_DEAD_MAX_BYTES: ${LINK_QUEUE_V3_DEAD_MAX_BYTES:-33554432}
LINK_QUEUE_V3_DEAD_RETENTION_MS: ${LINK_QUEUE_V3_DEAD_RETENTION_MS:-2592000000}
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:-}
HEALTH_VOLUME_PATHS: database=/health-volumes/database=${HEALTH_DATABASE_BUDGET_BYTES:-322122547200},redis=/health-volumes/redis=${HEALTH_REDIS_BUDGET_BYTES:-4294967296},terrain=/health-volumes/terrain=${HEALTH_TERRAIN_BUDGET_BYTES:-53687091200},logs=/health-volumes/logs=${HEALTH_LOG_BUDGET_BYTES:-21474836480}
DATA_LIFECYCLE_RETENTION_ENABLED: ${DATA_LIFECYCLE_RETENTION_ENABLED:-false}
DATA_LIFECYCLE_COMPRESSION_ENABLED: ${DATA_LIFECYCLE_COMPRESSION_ENABLED:-false}
DATA_LIFECYCLE_RETENTION_TARGETS: ${DATA_LIFECYCLE_RETENTION_TARGETS:-}
DATA_LIFECYCLE_RESTORE_RECEIPT_PATH: /backup-receipts/latest.json
DATA_LIFECYCLE_RESTORE_RECEIPT_SIGNATURE_PATH: /backup-receipts/latest.json.sig
DATA_LIFECYCLE_RECEIPT_VERIFY_KEY_PATH: /backup-receipts/verify.pem
DATA_LIFECYCLE_RECEIPT_MAX_AGE_DAYS: ${DATA_LIFECYCLE_RECEIPT_MAX_AGE_DAYS:-7}
NODE_ENV: production
depends_on:
db-migrate:
condition: service_completed_successfully
ports:
- "127.0.0.1:3000:3000"
volumes:
- mosquitto_log:/mosquitto/log:ro
- ./mosquitto:/mosquitto/config
- timescaledb_data:/health-volumes/database:ro
- redis_data:/health-volumes/redis:ro
- srtm_data:/health-volumes/terrain:ro
- loki_data:/health-volumes/logs:ro
- ${BACKUP_RECEIPT_HOST_DIR:-./backup-receipts}:/backup-receipts:ro
group_add:
- "1883"
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
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, *application-hardening]
image: ${BACKEND_IMAGE:-meshcore-analytics-backend:local}
build:
context: .
dockerfile: Dockerfile.backend
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
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}
DATABASE_APPLICATION_NAME: meshcore-path-learning
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}
PATH_LEARNING_INTERVAL_MS: ${PATH_LEARNING_INTERVAL_MS:-3600000}
NODE_ENV: production
depends_on:
db-migrate:
condition: service_completed_successfully
path-history-worker:
<<: [*internal-hosts, *application-hardening]
image: ${BACKEND_IMAGE:-meshcore-analytics-backend:local}
build:
context: .
dockerfile: Dockerfile.backend
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
restart: always
logging: *json-log-limits
# A full 12k-packet history rebuild exceeds Node's cgroup-derived ~256 MB
# heap default while holding topology context and concurrent trellises.
mem_limit: "768m"
memswap_limit: "768m"
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_CONNECTION_TIMEOUT_MS: ${PATH_HISTORY_DATABASE_CONNECTION_TIMEOUT_MS:-30000}
DATABASE_SKIP_SCHEMA_INIT: "true"
PATH_HISTORY_RUN_DEADLINE_MS: ${PATH_HISTORY_RUN_DEADLINE_MS:-7200000}
PATH_HISTORY_BATCH_SIZE: ${PATH_HISTORY_BATCH_SIZE:-64}
PATH_HISTORY_RETRY_INTERVAL_MS: ${PATH_HISTORY_RETRY_INTERVAL_MS:-300000}
PATH_HISTORY_SEGMENT_COUNTER_CAPACITY: ${PATH_HISTORY_SEGMENT_COUNTER_CAPACITY:-65536}
PATH_HISTORY_MAX_OLD_SPACE_MB: ${PATH_HISTORY_MAX_OLD_SPACE_MB:-384}
NODE_OPTIONS: --max-old-space-size=${PATH_HISTORY_MAX_OLD_SPACE_MB:-384}
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:
db-migrate:
condition: service_completed_successfully
deploy:
resources:
limits:
cpus: "1.0"
memory: 768M
health-worker:
<<: [*internal-hosts, *application-hardening]
image: ${BACKEND_IMAGE:-meshcore-analytics-backend:local}
build:
context: .
dockerfile: Dockerfile.backend
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
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}
DATABASE_APPLICATION_NAME: meshcore-health-worker
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}
HEALTH_VOLUME_PATHS: database=/health-volumes/database=${HEALTH_DATABASE_BUDGET_BYTES:-322122547200},redis=/health-volumes/redis=${HEALTH_REDIS_BUDGET_BYTES:-4294967296},terrain=/health-volumes/terrain=${HEALTH_TERRAIN_BUDGET_BYTES:-53687091200},logs=/health-volumes/logs=${HEALTH_LOG_BUDGET_BYTES:-21474836480}
DATA_LIFECYCLE_RETENTION_ENABLED: "true"
DATA_LIFECYCLE_RETENTION_TARGETS: worker_health_snapshots,operational_check_results
NODE_ENV: production
depends_on:
db-migrate:
condition: service_completed_successfully
volumes:
- timescaledb_data:/health-volumes/database:ro
- redis_data:/health-volumes/redis:ro
- srtm_data:/health-volumes/terrain:ro
- loki_data:/health-volumes/logs:ro
synthetic-monitor:
<<: [*internal-hosts, *application-hardening]
image: ${BACKEND_IMAGE:-meshcore-analytics-backend:local}
build:
context: .
dockerfile: Dockerfile.backend
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
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_APPLICATION_NAME: meshcore-synthetic-monitor
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:-http://alert-receiver:8080/alerts}
NODE_ENV: production
depends_on:
db-migrate:
condition: service_completed_successfully
backend:
condition: service_healthy
alert-receiver:
condition: service_healthy
link-backfill-worker:
<<: [*internal-hosts, *application-hardening]
image: ${BACKEND_IMAGE:-meshcore-analytics-backend:local}
build:
context: .
dockerfile: Dockerfile.backend
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
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:
db-migrate:
condition: service_completed_successfully
backend:
condition: service_healthy
link-worker:
condition: service_healthy
link-worker:
<<: [*internal-hosts, *application-hardening]
image: ${RF_WORKER_IMAGE:-meshcore-analytics-rf-worker:local}
build:
context: ./viewshed-worker
dockerfile: Dockerfile
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
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}
DATABASE_APPLICATION_NAME: meshcore-link-worker
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}
LINK_QUEUE_V3_DEAD_MAX_JOBS: ${LINK_QUEUE_V3_DEAD_MAX_JOBS:-1000}
LINK_QUEUE_V3_DEAD_MAX_BYTES: ${LINK_QUEUE_V3_DEAD_MAX_BYTES:-33554432}
LINK_QUEUE_V3_DEAD_RETENTION_MS: ${LINK_QUEUE_V3_DEAD_RETENTION_MS:-2592000000}
SRTM_MAX_LINK_TILES: ${SRTM_MAX_LINK_TILES:-64}
SRTM_CACHE_MAX_BYTES: ${SRTM_CACHE_MAX_BYTES:-21474836480}
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:
db-migrate:
condition: service_completed_successfully
# Canonical whole-region RF calculator. HopReach owns propagation and
# raster output; UK Mesh supplies only the internal data adapter, versioned
# boundary, orchestration profile, and native map consumer.
hopreach:
<<: *internal-hosts
image: ${HOPREACH_IMAGE:-meshcore-analytics-hopreach:local}
build:
context: ./third_party/hopreach
dockerfile: Dockerfile
args:
VERSION: v0.1.32-ukmesh.3
SOURCE_REVISION: ${SOURCE_REVISION:-local}
restart: always
logging: *json-log-limits
cpus: ${HOPREACH_CPUS:-4.0}
mem_limit: ${HOPREACH_MEMORY_LIMIT:-8g}
memswap_limit: ${HOPREACH_MEMORY_LIMIT:-8g}
environment:
HOPREACH_CONFIG: /config/config.yaml
GOMAXPROCS: ${HOPREACH_CPU_WORKERS:-4}
volumes:
- rf_coverage_data:/data
- ./rf-coverage/config.ukmesh.yaml:/config/config.yaml:ro
- ./rf-coverage/boundaries/uk-operational-v1.geojson:/config/uk-operational-v1.geojson:ro
depends_on:
backend:
condition: service_healthy
app-ukmesh:
<<: [*internal-hosts, *application-hardening]
image: ${APP_IMAGE:-meshcore-analytics-app:local}
build:
context: .
dockerfile: Dockerfile.app
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
VITE_APP_HOSTNAME: app.ukmesh.com
VITE_BUILD_TARGET: app
VITE_NETWORK: ukmesh
VITE_SITE: ukmesh
VITE_RF_COVERAGE_ENABLED: ${RF_COVERAGE_ENABLED:-true}
restart: always
logging: *json-log-limits
mem_limit: "128m"
memswap_limit: "128m"
depends_on:
backend:
condition: service_healthy
ports:
- "127.0.0.1:3003:8080"
networks:
default:
ipv4_address: 172.30.0.10
volumes:
- ${TERRAIN_TILES_PATH:-./terrain-tiles}:/terrain-tiles:ro
- rf_coverage_data:/rf-coverage-data:ro
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m,mode=1777
- /var/cache/nginx:rw,noexec,nosuid,size=32m,uid=101,gid=101,mode=0755
- /var/run:rw,noexec,nosuid,size=4m,uid=101,gid=101,mode=0755
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ >/dev/null && wget -qO- http://127.0.0.1:8080/readyz >/dev/null"]
interval: 30s
timeout: 10s
retries: 6
start_period: 15s
website-ukmesh:
<<: [*internal-hosts, *application-hardening]
image: ${WEBSITE_IMAGE:-meshcore-analytics-website:local}
build:
context: .
dockerfile: Dockerfile.website
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
VITE_APP_HOSTNAME: app.ukmesh.com
VITE_BUILD_TARGET: public-website
VITE_SITE: ukmesh
restart: always
logging: *json-log-limits
mem_limit: "128m"
memswap_limit: "128m"
ports:
- "127.0.0.1:3004:8080"
networks:
default:
ipv4_address: 172.30.0.11
depends_on:
backend:
condition: service_healthy
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m,mode=1777
- /var/cache/nginx:rw,noexec,nosuid,size=32m,uid=101,gid=101,mode=0755
- /var/run:rw,noexec,nosuid,size=4m,uid=101,gid=101,mode=0755
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ >/dev/null && wget -qO- http://127.0.0.1:8080/readyz >/dev/null"]
interval: 30s
timeout: 10s
retries: 6
start_period: 15s
mesh-health-check-data-init:
<<: *application-hardening
image: ${HEALTHCHECK_IMAGE:-meshcore-analytics-health-check:local}
user: "0:0"
entrypoint: ["/bin/sh", "-ec"]
command:
- |
touch /app/data/.meshcore-initialized
chown -R node:node /app/data
find /app/data -xdev -type f -exec chmod 0640 {} +
chmod 0750 /app/data
restart: "no"
network_mode: "none"
mem_limit: "64m"
memswap_limit: "64m"
cap_add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
volumes:
- mesh_health_check_data:/app/data
mesh-health-check:
<<: [*internal-hosts, *application-hardening]
image: ${HEALTHCHECK_IMAGE:-meshcore-analytics-health-check:local}
build:
context: .
dockerfile: Dockerfile.mesh-health-check
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
MESH_HEALTH_CHECK_REF: ${HEALTHCHECK_SOURCE_REF:?HEALTHCHECK_SOURCE_REF full commit required}
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:
mesh-health-check-data-init:
condition: service_completed_successfully
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, *application-hardening]
image: ${WEBSITE_DEV_IMAGE:-meshcore-analytics-website-dev:local}
build:
context: .
dockerfile: Dockerfile.website
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
VITE_APP_HOSTNAME: ${VITE_DEV_APP_HOSTNAME:-test.ukmesh.com}
VITE_BUILD_TARGET: dev-website
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}
restart: always
profiles: [dev]
logging: *json-log-limits
mem_limit: "128m"
memswap_limit: "128m"
ports:
- "127.0.0.1:3006:8080"
networks:
default:
ipv4_address: 172.30.0.12
depends_on:
backend:
condition: service_healthy
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m,mode=1777
- /var/cache/nginx:rw,noexec,nosuid,size=32m,uid=101,gid=101,mode=0755
- /var/run:rw,noexec,nosuid,size=4m,uid=101,gid=101,mode=0755
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ >/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:8080"
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:8080"
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@sha256:e39ee8da81ad5e05d77f38d2f51c60ca51bf2a8450ac3abab50c17fdb91d91bf
restart: always
logging: *json-log-limits
mem_limit: "256m"
memswap_limit: "256m"
user: "65532:65532"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m,mode=1777
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@sha256:5fe9fa99e9a747297cdf0239a5b25d192d8f668bd6505b09beef4dffcab5aac2
restart: always
logging: *json-log-limits
mem_limit: "512m"
memswap_limit: "512m"
command: -config.file=/etc/loki/loki.yaml
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,size=32m
volumes:
- ./logging/loki.yaml:/etc/loki/loki.yaml:ro
- loki_data:/loki
healthcheck:
test: ["CMD", "/busybox/wget", "-q", "-O", "/dev/null", "http://127.0.0.1:3100/ready"]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
alloy:
<<: *internal-hosts
image: grafana/alloy@sha256:491b0578c04983fd54fe99b587b6fab4404dc46d0dc16677bd6b00cc1140b308
restart: always
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
mem_limit: "256m"
memswap_limit: "256m"
user: "473:473"
group_add:
- "${JOURNAL_GID:-999}"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
command:
- run
- --server.http.listen-addr=0.0.0.0:12345
- --storage.path=/var/lib/alloy/data
- /etc/alloy/config.alloy
volumes:
- ./logging/alloy/config.alloy:/etc/alloy/config.alloy:ro
- /var/log/journal:/var/log/journal:ro
- /run/log/journal:/run/log/journal:ro
- /etc/machine-id:/etc/machine-id:ro
- alloy_data:/var/lib/alloy
healthcheck:
test: ["CMD", "/bin/alloy", "validate", "/etc/alloy/config.alloy"]
interval: 30s
timeout: 10s
retries: 5
start_period: 10s
depends_on:
loki:
condition: service_healthy
prometheus:
<<: *internal-hosts
image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
restart: always
logging: *json-log-limits
mem_limit: "256m"
memswap_limit: "256m"
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=15d
- --storage.tsdb.retention.size=8GB
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,size=32m
volumes:
- ./logging/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./logging/rules:/etc/prometheus/rules:ro
- prometheus_data:/prometheus
ports:
- "127.0.0.1:9090:9090"
healthcheck:
test: ["CMD", "/bin/wget", "-q", "-O", "/dev/null", "http://127.0.0.1:9090/-/ready"]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
depends_on:
backend:
condition: service_healthy
alertmanager:
condition: service_healthy
postgres-exporter:
condition: service_healthy
redis-exporter:
condition: service_healthy
node-exporter:
condition: service_healthy
blackbox-exporter:
condition: service_healthy
grafana:
<<: *internal-hosts
image: grafana/grafana@sha256:0a2874cf39c6487093c682215f7c7903ed8646d78ae5f911af945d2dfcc0a447
restart: always
logging: *json-log-limits
mem_limit: "512m"
memswap_limit: "512m"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
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"
healthcheck:
test: ["CMD", "/usr/bin/wget", "-q", "-O", "/dev/null", "http://127.0.0.1:3000/api/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
depends_on:
loki:
condition: service_healthy
prometheus:
condition: service_healthy
alert-receiver:
<<: *internal-hosts
image: ${BACKEND_IMAGE:-meshcore-analytics-backend:local}
build:
context: .
dockerfile: Dockerfile.backend
args:
SOURCE_REVISION: ${SOURCE_REVISION:-local}
command: ["node", "dist/workers/alert-receiver.js"]
restart: always
logging: *json-log-limits
mem_limit: "128m"
memswap_limit: "128m"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m
environment:
DATABASE_APPLICATION_NAME: meshcore-alert-receiver
ALERT_FORWARD_URL: ${ALERT_FORWARD_URL:-}
ALERT_RECEIVER_PATH: /var/lib/meshcore-alerts/alerts.jsonl
NODE_ENV: production
volumes:
- alert_receiver_data:/var/lib/meshcore-alerts
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null"]
interval: 30s
timeout: 5s
retries: 5
start_period: 10s
alertmanager:
<<: *internal-hosts
image: prom/alertmanager@sha256:27c475db5fb156cab31d5c18a4251ac7ed567746a2483ff264516437a39b15ba
restart: always
logging: *json-log-limits
mem_limit: "128m"
memswap_limit: "128m"
command:
- --config.file=/etc/alertmanager/alertmanager.yml
- --storage.path=/alertmanager
- --data.retention=120h
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m
volumes:
- ./logging/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
- alertmanager_data:/alertmanager
healthcheck:
test: ["CMD", "/bin/wget", "-q", "-O", "/dev/null", "http://127.0.0.1:9093/-/ready"]
interval: 30s
timeout: 5s
retries: 5
start_period: 10s
depends_on:
alert-receiver:
condition: service_healthy
postgres-exporter:
<<: *internal-hosts
image: quay.io/prometheuscommunity/postgres-exporter@sha256:fb96c4413985d4b23ab02b19022b3d70a86c8e0a62f41ab15ebb6f4673781a5d
restart: always
logging: *json-log-limits
mem_limit: "128m"
memswap_limit: "128m"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment:
DATA_SOURCE_NAME: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore}?sslmode=disable
healthcheck:
test: ["CMD", "/bin/postgres_exporter", "--version"]
interval: 30s
timeout: 5s
retries: 5
redis-exporter:
<<: *internal-hosts
image: oliver006/redis_exporter@sha256:4c8000eb3525e0f6ed1327499861e272fcce358a9e7a87dd9db74c44607e6d8e
restart: always
logging: *json-log-limits
mem_limit: "128m"
memswap_limit: "128m"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
environment:
REDIS_ADDR: redis://redis:6379
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD required}
REDIS_EXPORTER_CHECK_SINGLE_KEYS: meshcore:link:v3:worker_heartbeat
healthcheck:
test: ["CMD", "/redis_exporter", "-version"]
interval: 30s
timeout: 5s
retries: 5
node-exporter:
<<: *internal-hosts
image: prom/node-exporter@sha256:d00a542e409ee618a4edc67da14dd48c5da66726bbd5537ab2af9c1dfc442c8a
restart: always
logging: *json-log-limits
mem_limit: "128m"
memswap_limit: "128m"
pid: host
command:
- --path.rootfs=/host
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
- --collector.filesystem.mount-points-exclude=^/(dev|proc|sys|var/lib/docker/.+)($|/)
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- /:/host:ro,rslave
healthcheck:
test: ["CMD", "/bin/node_exporter", "--version"]
interval: 30s
timeout: 5s
retries: 5
blackbox-exporter:
<<: *internal-hosts
image: prom/blackbox-exporter@sha256:a50c4c0eda297baa1678cd4dc4712a67fdea713b832d43ce7fcc5f9bea05094d
restart: always
logging: *json-log-limits
mem_limit: "128m"
memswap_limit: "128m"
command: ["--config.file=/etc/blackbox_exporter/config.yml"]
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- ./logging/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
healthcheck:
test: ["CMD", "/bin/blackbox_exporter", "--version"]
interval: 30s
timeout: 5s
retries: 5
networks:
default:
ipam:
config:
# Fixed proxy addresses make the backend's immediate-peer trust exact.
# Keep Docker's dynamic allocator away from those reserved low
# addresses when the full stack starts services concurrently.
- subnet: 172.30.0.0/24
ip_range: 172.30.0.128/25
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:
alloy_data:
alert_receiver_data:
alertmanager_data:
mesh_health_check_data:
rf_coverage_data: