Files
meshcore-hub/docker-compose.yml
T

359 lines
14 KiB
YAML

services:
# ==========================================================================
# MQTT Broker - MeshCore MQTT Broker (optional, use --profile mqtt)
# WebSocket-only broker with MeshCore public key authentication
# Most users will connect to an external MQTT broker instead
# See: https://github.com/michaelhart/meshcore-mqtt-broker
# ==========================================================================
mqtt:
image: ghcr.io/ipnet-mesh/meshcore-mqtt-broker:latest
container_name: ${COMPOSE_PROJECT_NAME:-hub}-mqtt
profiles:
- all
- mqtt
restart: unless-stopped
volumes:
- mqtt_data:/data
environment:
# Broker listener
- MQTT_WS_PORT=${MQTT_PORT:-1883}
- MQTT_HOST=0.0.0.0
# JWT audience validation
- AUTH_EXPECTED_AUDIENCE=${MQTT_TOKEN_AUDIENCE:-mqtt.localhost}
# Subscriber accounts (hub connects as role 1 = admin for /internal topics)
- SUBSCRIBER_MAX_CONNECTIONS_DEFAULT=5
- SUBSCRIBER_1=${MQTT_USERNAME:-admin}:${MQTT_PASSWORD:-admin}:1
# Abuse detection
- ABUSE_ENFORCEMENT_ENABLED=false
- ABUSE_DUPLICATE_WINDOW_SIZE=100
- ABUSE_DUPLICATE_WINDOW_MS=300000
- ABUSE_DUPLICATE_THRESHOLD=10
- ABUSE_MAX_DUPLICATES_PER_PACKET=5
- ABUSE_DUPLICATE_RATE_THRESHOLD=0.3
- ABUSE_DUPLICATE_RATE_WINDOW_MS=300000
- ABUSE_BUCKET_CAPACITY=20
- ABUSE_BUCKET_REFILL_RATE=3
- ABUSE_MAX_PACKET_SIZE=255
- ABUSE_MAX_TOPICS_PER_DAY=3
- ABUSE_ANOMALY_THRESHOLD=10
- ABUSE_MAX_IATA_CHANGES_24H=3
- ABUSE_TOPIC_HISTORY_SIZE=50
- ABUSE_TOPIC_HISTORY_WINDOW_MS=86400000
- ABUSE_PERSISTENCE_PATH=/data/abuse-detection.db
- ABUSE_PERSISTENCE_INTERVAL_MS=300000
healthcheck:
test:
[
"CMD",
"node",
"-e",
"const net=require('net');const s=net.createConnection(process.env.MQTT_WS_PORT||1883,'127.0.0.1',()=>{s.end();process.exit(0)});s.on('error',()=>process.exit(1));setTimeout(()=>process.exit(1),3000)",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# ==========================================================================
# Observer - MeshCore packet capture to MQTT (serial only)
# Uses ghcr.io/agessaman/meshcore-packet-capture (separate project)
# Publishes to local MQTT (broker 3) for the hub collector to ingest.
# Optionally publish to Let's Mesh (brokers 1 & 2) for cloud map integration.
# ==========================================================================
observer:
image: ghcr.io/agessaman/meshcore-packet-capture:${PACKETCAPTURE_IMAGE_VERSION:-latest}
container_name: ${COMPOSE_PROJECT_NAME:-hub}-observer
profiles:
- all
- observer
depends_on:
mqtt:
condition: service_healthy
restart: unless-stopped
devices:
- "${SERIAL_PORT:-/dev/ttyUSB0}:${SERIAL_PORT:-/dev/ttyUSB0}"
user: root
environment:
- LOG_LEVEL=${LOG_LEVEL:-INFO}
# Connection (serial only)
- PACKETCAPTURE_CONNECTION_TYPE=serial
- PACKETCAPTURE_SERIAL_PORTS=${SERIAL_PORT:-/dev/ttyUSB0}
- PACKETCAPTURE_TIMEOUT=${PACKETCAPTURE_TIMEOUT:-30}
- PACKETCAPTURE_MAX_CONNECTION_RETRIES=${PACKETCAPTURE_MAX_CONNECTION_RETRIES:-5}
- PACKETCAPTURE_CONNECTION_RETRY_DELAY=${PACKETCAPTURE_CONNECTION_RETRY_DELAY:-5}
- PACKETCAPTURE_HEALTH_CHECK_INTERVAL=${PACKETCAPTURE_HEALTH_CHECK_INTERVAL:-30}
# Device
- PACKETCAPTURE_IATA=${PACKETCAPTURE_IATA:-LOC}
- PACKETCAPTURE_ORIGIN=${PACKETCAPTURE_ORIGIN:-}
- PACKETCAPTURE_ADVERT_INTERVAL_HOURS=${PACKETCAPTURE_ADVERT_INTERVAL_HOURS:-11}
- PACKETCAPTURE_RF_DATA_TIMEOUT=${PACKETCAPTURE_RF_DATA_TIMEOUT:-15.0}
# MQTT Broker 1 - Let's Mesh US (opt-in)
- PACKETCAPTURE_MQTT1_ENABLED=${PACKETCAPTURE_MQTT1_ENABLED:-false}
- PACKETCAPTURE_MQTT1_SERVER=${PACKETCAPTURE_MQTT1_SERVER:-mqtt-us-v1.letsmesh.net}
- PACKETCAPTURE_MQTT1_PORT=${PACKETCAPTURE_MQTT1_PORT:-443}
- PACKETCAPTURE_MQTT1_TRANSPORT=websockets
- PACKETCAPTURE_MQTT1_USE_TLS=${PACKETCAPTURE_MQTT1_USE_TLS:-true}
- PACKETCAPTURE_MQTT1_USE_AUTH_TOKEN=${PACKETCAPTURE_MQTT1_USE_AUTH_TOKEN:-true}
- PACKETCAPTURE_MQTT1_TOKEN_AUDIENCE=${PACKETCAPTURE_MQTT1_TOKEN_AUDIENCE:-mqtt-us-v1.letsmesh.net}
- PACKETCAPTURE_MQTT1_KEEPALIVE=${PACKETCAPTURE_MQTT1_KEEPALIVE:-120}
# MQTT Broker 2 - Let's Mesh EU (opt-in)
- PACKETCAPTURE_MQTT2_ENABLED=${PACKETCAPTURE_MQTT2_ENABLED:-false}
- PACKETCAPTURE_MQTT2_SERVER=${PACKETCAPTURE_MQTT2_SERVER:-mqtt-eu-v1.letsmesh.net}
- PACKETCAPTURE_MQTT2_PORT=${PACKETCAPTURE_MQTT2_PORT:-443}
- PACKETCAPTURE_MQTT2_TRANSPORT=websockets
- PACKETCAPTURE_MQTT2_USE_TLS=${PACKETCAPTURE_MQTT2_USE_TLS:-true}
- PACKETCAPTURE_MQTT2_USE_AUTH_TOKEN=${PACKETCAPTURE_MQTT2_USE_AUTH_TOKEN:-true}
- PACKETCAPTURE_MQTT2_TOKEN_AUDIENCE=${PACKETCAPTURE_MQTT2_TOKEN_AUDIENCE:-mqtt-eu-v1.letsmesh.net}
- PACKETCAPTURE_MQTT2_KEEPALIVE=${PACKETCAPTURE_MQTT2_KEEPALIVE:-120}
# MQTT Broker 3 - Local MQTT (wired to hub's MQTT_* vars)
- PACKETCAPTURE_MQTT3_ENABLED=${PACKETCAPTURE_MQTT3_ENABLED:-true}
- PACKETCAPTURE_MQTT3_SERVER=${MQTT_HOST}
- PACKETCAPTURE_MQTT3_PORT=${MQTT_PORT}
- PACKETCAPTURE_MQTT3_USERNAME=${MQTT_USERNAME:-}
- PACKETCAPTURE_MQTT3_PASSWORD=${MQTT_PASSWORD:-}
- PACKETCAPTURE_MQTT3_TRANSPORT=websockets
- PACKETCAPTURE_MQTT3_USE_TLS=${MQTT_TLS:-false}
- PACKETCAPTURE_MQTT3_USE_AUTH_TOKEN=true
- PACKETCAPTURE_MQTT3_TOKEN_AUDIENCE=${MQTT_TOKEN_AUDIENCE:-mqtt.localhost}
- PACKETCAPTURE_MQTT3_KEEPALIVE=${PACKETCAPTURE_MQTT3_KEEPALIVE:-60}
# Topics match broker's <prefix>/<IATA>/<pubkey>/<feed> format
- PACKETCAPTURE_MQTT3_TOPIC_STATUS=meshcore/{IATA}/{PUBLIC_KEY}/status
- PACKETCAPTURE_MQTT3_TOPIC_PACKETS=meshcore/{IATA}/{PUBLIC_KEY}/packets
# MQTT reconnection
- PACKETCAPTURE_MAX_MQTT_RETRIES=${PACKETCAPTURE_MAX_MQTT_RETRIES:-5}
- PACKETCAPTURE_MQTT_RETRY_DELAY=${PACKETCAPTURE_MQTT_RETRY_DELAY:-5}
- PACKETCAPTURE_EXIT_ON_RECONNECT_FAIL=${PACKETCAPTURE_EXIT_ON_RECONNECT_FAIL:-true}
volumes:
- observer_data:/app/data
# ==========================================================================
# Collector - MQTT subscriber and database storage
# ==========================================================================
collector:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: ${COMPOSE_PROJECT_NAME:-hub}-collector
profiles:
- all
- core
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
volumes:
- data:/data
- ${SEED_HOME:-./seed}:/seed
environment:
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- MQTT_HOST=${MQTT_HOST:-mqtt}
- MQTT_PORT=${MQTT_PORT:-1883}
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
- MQTT_TLS=${MQTT_TLS:-false}
- MQTT_TRANSPORT=${MQTT_TRANSPORT:-websockets}
- MQTT_WS_PATH=${MQTT_WS_PATH:-/}
- COLLECTOR_CHANNEL_KEYS=${COLLECTOR_CHANNEL_KEYS:-}
- COLLECTOR_INCLUDE_TEST_CHANNEL=${COLLECTOR_INCLUDE_TEST_CHANNEL:-false}
- DATA_HOME=/data
- SEED_HOME=/seed
# Webhook configuration
- WEBHOOK_ADVERTISEMENT_URL=${WEBHOOK_ADVERTISEMENT_URL:-}
- WEBHOOK_ADVERTISEMENT_SECRET=${WEBHOOK_ADVERTISEMENT_SECRET:-}
- WEBHOOK_MESSAGE_URL=${WEBHOOK_MESSAGE_URL:-}
- WEBHOOK_MESSAGE_SECRET=${WEBHOOK_MESSAGE_SECRET:-}
- WEBHOOK_CHANNEL_MESSAGE_URL=${WEBHOOK_CHANNEL_MESSAGE_URL:-}
- WEBHOOK_CHANNEL_MESSAGE_SECRET=${WEBHOOK_CHANNEL_MESSAGE_SECRET:-}
- WEBHOOK_DIRECT_MESSAGE_URL=${WEBHOOK_DIRECT_MESSAGE_URL:-}
- WEBHOOK_DIRECT_MESSAGE_SECRET=${WEBHOOK_DIRECT_MESSAGE_SECRET:-}
- WEBHOOK_TIMEOUT=${WEBHOOK_TIMEOUT:-10.0}
- WEBHOOK_MAX_RETRIES=${WEBHOOK_MAX_RETRIES:-3}
- WEBHOOK_RETRY_BACKOFF=${WEBHOOK_RETRY_BACKOFF:-2.0}
# Data retention and cleanup configuration
- DATA_RETENTION_ENABLED=${DATA_RETENTION_ENABLED:-true}
- DATA_RETENTION_DAYS=${DATA_RETENTION_DAYS:-30}
- DATA_RETENTION_INTERVAL_HOURS=${DATA_RETENTION_INTERVAL_HOURS:-24}
- NODE_CLEANUP_ENABLED=${NODE_CLEANUP_ENABLED:-true}
- NODE_CLEANUP_DAYS=${NODE_CLEANUP_DAYS:-7}
command: ["collector"]
healthcheck:
test: ["CMD", "meshcore-hub", "health", "collector"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# ==========================================================================
# API Server - REST API for querying data
# ==========================================================================
api:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: ${COMPOSE_PROJECT_NAME:-hub}-api
profiles:
- all
- core
restart: unless-stopped
depends_on:
migrate:
condition: service_completed_successfully
collector:
condition: service_started
volumes:
- data:/data
environment:
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- MQTT_HOST=${MQTT_HOST:-mqtt}
- MQTT_PORT=${MQTT_PORT:-1883}
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
- MQTT_TLS=${MQTT_TLS:-false}
- MQTT_TRANSPORT=${MQTT_TRANSPORT:-websockets}
- MQTT_WS_PATH=${MQTT_WS_PATH:-/}
- DATA_HOME=/data
- API_HOST=0.0.0.0
- API_PORT=8000
- API_READ_KEY=${API_READ_KEY:-}
- API_ADMIN_KEY=${API_ADMIN_KEY:-}
- METRICS_ENABLED=${METRICS_ENABLED:-true}
- METRICS_CACHE_TTL=${METRICS_CACHE_TTL:-60}
command: ["api"]
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# ==========================================================================
# Web Dashboard - Web interface for network visualization
# ==========================================================================
web:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: ${COMPOSE_PROJECT_NAME:-hub}-web
profiles:
- all
- core
restart: unless-stopped
depends_on:
api:
condition: service_healthy
volumes:
- ${CONTENT_HOME:-./content}:/content:ro
environment:
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- API_BASE_URL=http://api:8000
# Use ADMIN key to allow write operations from admin interface
# Falls back to READ key if ADMIN key is not set
- API_KEY=${API_ADMIN_KEY:-${API_READ_KEY:-}}
- WEB_HOST=0.0.0.0
- WEB_PORT=8080
- WEB_THEME=${WEB_THEME:-dark}
- WEB_LOCALE=${WEB_LOCALE:-en}
- WEB_DATETIME_LOCALE=${WEB_DATETIME_LOCALE:-en-US}
- WEB_ADMIN_ENABLED=${WEB_ADMIN_ENABLED:-false}
- NETWORK_NAME=${NETWORK_NAME:-MeshCore Network}
- NETWORK_CITY=${NETWORK_CITY:-}
- NETWORK_COUNTRY=${NETWORK_COUNTRY:-}
- NETWORK_RADIO_CONFIG=${NETWORK_RADIO_CONFIG:-}
- NETWORK_CONTACT_EMAIL=${NETWORK_CONTACT_EMAIL:-}
- NETWORK_CONTACT_DISCORD=${NETWORK_CONTACT_DISCORD:-}
- NETWORK_CONTACT_GITHUB=${NETWORK_CONTACT_GITHUB:-}
- NETWORK_CONTACT_YOUTUBE=${NETWORK_CONTACT_YOUTUBE:-}
- NETWORK_WELCOME_TEXT=${NETWORK_WELCOME_TEXT:-}
- CONTENT_HOME=/content
- TZ=${TZ:-UTC}
- COLLECTOR_CHANNEL_KEYS=${COLLECTOR_CHANNEL_KEYS:-}
- COLLECTOR_INCLUDE_TEST_CHANNEL=${COLLECTOR_INCLUDE_TEST_CHANNEL:-false}
# Feature flags (set to false to disable specific pages)
- FEATURE_DASHBOARD=${FEATURE_DASHBOARD:-true}
- FEATURE_NODES=${FEATURE_NODES:-true}
- FEATURE_ADVERTISEMENTS=${FEATURE_ADVERTISEMENTS:-true}
- FEATURE_MESSAGES=${FEATURE_MESSAGES:-true}
- FEATURE_MAP=${FEATURE_MAP:-true}
- FEATURE_MEMBERS=${FEATURE_MEMBERS:-true}
- FEATURE_PAGES=${FEATURE_PAGES:-true}
command: ["web"]
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://localhost:8080/health')",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# ==========================================================================
# Database Migrations - Run Alembic migrations
# ==========================================================================
migrate:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: ${COMPOSE_PROJECT_NAME:-hub}-migrate
profiles:
- all
- core
- migrate
restart: "no"
volumes:
- data:/data
environment:
- DATA_HOME=/data
command: ["db", "upgrade"]
# ==========================================================================
# Seed Data - Import node_tags.yaml and members.yaml from SEED_HOME
# NOTE: This is NOT run automatically. Use --profile seed to run explicitly.
# Since tags are now managed via the admin UI, automatic seeding would
# overwrite user changes.
# ==========================================================================
seed:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: ${COMPOSE_PROJECT_NAME:-hub}-seed
profiles:
- seed
restart: "no"
volumes:
- data:/data
- ${SEED_HOME:-./seed}:/seed:ro
environment:
- DATA_HOME=/data
- SEED_HOME=/seed
- LOG_LEVEL=${LOG_LEVEL:-INFO}
# Imports both node_tags.yaml and members.yaml if they exist
command: ["collector", "seed"]
# ==========================================================================
# Volumes
# ==========================================================================
volumes:
data:
name: ${COMPOSE_PROJECT_NAME:-hub}_data
mqtt_data:
name: ${COMPOSE_PROJECT_NAME:-hub}_mqtt_data
observer_data:
name: ${COMPOSE_PROJECT_NAME:-hub}_observer_data