Files
meshcore-hub/docker-compose.yml
Louis King 0f50bf4a41 Add custom markdown pages feature to web dashboard
Allows adding static content pages (About, FAQ, etc.) as markdown files
with YAML frontmatter. Pages are stored in PAGES_HOME directory (default:
./pages), automatically appear in navigation menu, and are included in
the sitemap.

- Add PageLoader class to parse markdown with frontmatter
- Add /pages/{slug} route for rendering custom pages
- Add PAGES_HOME config setting to WebSettings
- Add prose CSS styles for markdown content
- Add pages to navigation and sitemap
- Update docker-compose.yml with pages volume mount
- Add comprehensive tests for PageLoader and routes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 18:36:23 +00:00

327 lines
11 KiB
YAML

services:
# ==========================================================================
# MQTT Broker - Eclipse Mosquitto (optional, use --profile mqtt)
# Most users will connect to an external MQTT broker instead
# ==========================================================================
mqtt:
image: eclipse-mosquitto:2
container_name: meshcore-mqtt
profiles:
- all
- mqtt
restart: unless-stopped
ports:
- "${MQTT_EXTERNAL_PORT:-1883}:1883"
- "${MQTT_WS_PORT:-9001}:9001"
volumes:
# - ./etc/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
- mosquitto_data:/mosquitto/data
- mosquitto_log:/mosquitto/log
healthcheck:
test: ["CMD", "mosquitto_sub", "-t", "$$SYS/#", "-C", "1", "-i", "healthcheck", "-W", "3"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# ==========================================================================
# Interface Receiver - MeshCore device to MQTT bridge (events)
# ==========================================================================
interface-receiver:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: meshcore-interface-receiver
profiles:
- all
- receiver
restart: unless-stopped
devices:
- "${SERIAL_PORT:-/dev/ttyUSB0}:${SERIAL_PORT:-/dev/ttyUSB0}"
user: root # Required for device access
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}
- SERIAL_PORT=${SERIAL_PORT:-/dev/ttyUSB0}
- SERIAL_BAUD=${SERIAL_BAUD:-115200}
- NODE_ADDRESS=${NODE_ADDRESS:-}
command: ["interface", "receiver"]
healthcheck:
test: ["CMD", "meshcore-hub", "health", "interface"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# ==========================================================================
# Interface Sender - MQTT to MeshCore device bridge (commands)
# ==========================================================================
interface-sender:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: meshcore-interface-sender
profiles:
- all
- sender
restart: unless-stopped
devices:
- "${SERIAL_PORT_SENDER:-/dev/ttyUSB1}:${SERIAL_PORT_SENDER:-/dev/ttyUSB1}"
user: root # Required for device access
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}
- SERIAL_PORT=${SERIAL_PORT_SENDER:-/dev/ttyUSB1}
- SERIAL_BAUD=${SERIAL_BAUD:-115200}
- NODE_ADDRESS=${NODE_ADDRESS_SENDER:-}
command: ["interface", "sender"]
healthcheck:
test: ["CMD", "meshcore-hub", "health", "interface"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# ==========================================================================
# Interface Mock Receiver - For testing without real devices
# ==========================================================================
interface-mock-receiver:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: meshcore-interface-mock-receiver
profiles:
- all
- mock
restart: unless-stopped
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}
- MOCK_DEVICE=true
- NODE_ADDRESS=${NODE_ADDRESS:-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef}
command: ["interface", "receiver", "--mock"]
healthcheck:
test: ["CMD", "meshcore-hub", "health", "interface"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# ==========================================================================
# Collector - MQTT subscriber and database storage
# ==========================================================================
collector:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: meshcore-collector
profiles:
- all
- core
restart: unless-stopped
depends_on:
db-migrate:
condition: service_completed_successfully
volumes:
- hub_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}
- 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 and sending commands
# ==========================================================================
api:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: meshcore-api
profiles:
- all
- core
restart: unless-stopped
depends_on:
db-migrate:
condition: service_completed_successfully
collector:
condition: service_started
ports:
- "${API_PORT:-8000}:8000"
volumes:
- hub_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}
- DATA_HOME=/data
- API_HOST=0.0.0.0
- API_PORT=8000
- API_READ_KEY=${API_READ_KEY:-}
- API_ADMIN_KEY=${API_ADMIN_KEY:-}
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: meshcore-web
profiles:
- all
- core
restart: unless-stopped
depends_on:
api:
condition: service_healthy
ports:
- "${WEB_PORT:-8080}:8080"
volumes:
- ${PAGES_HOME:-./pages}:/pages: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_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_WELCOME_TEXT=${NETWORK_WELCOME_TEXT:-}
- PAGES_HOME=/pages
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
# ==========================================================================
db-migrate:
image: ghcr.io/ipnet-mesh/meshcore-hub:${IMAGE_VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: meshcore-db-migrate
profiles:
- all
- core
- migrate
restart: "no"
volumes:
- hub_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: meshcore-seed
profiles:
- seed
restart: "no"
volumes:
- hub_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:
hub_data:
name: meshcore_hub_data
mosquitto_data:
name: meshcore_mosquitto_data
mosquitto_log:
name: meshcore_mosquitto_log