# Nodeledge MQTT listener stack (Portainer / docker compose compatible). # # Services: # db PostgreSQL holding the mqtt_messages table # mqtt-listener subscribes to MQTT, injects IMEI + device into JSON payloads, writes rows # api FastAPI read API over the database, protected by static app tokens # broker OPTIONAL test broker (compose profile "test") for trying the stack out # # Portainer: Stacks -> Add stack -> paste this file, then set the environment # variables (see .env.example) in the stack's "Environment variables" section. # Locally: cp .env.example .env && docker compose --profile test up -d --build services: db: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-nodeledge} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in the stack environment} POSTGRES_DB: ${POSTGRES_DB:-nodeledge} volumes: - db_data:/var/lib/postgresql/data # Creates the mqtt_messages table on the first database start only. - ./init/01_schema.sql:/docker-entrypoint-initdb.d/01_schema.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 10 start_period: 10s mqtt-listener: build: context: . dockerfile: listener/Dockerfile init: true restart: unless-stopped depends_on: db: condition: service_healthy environment: MQTT_BROKER_HOST: ${MQTT_BROKER_HOST:-broker} MQTT_BROKER_PORT: ${MQTT_BROKER_PORT:-1883} MQTT_USERNAME: ${MQTT_USERNAME:-} MQTT_PASSWORD: ${MQTT_PASSWORD:-} MQTT_CLIENT_ID: ${MQTT_CLIENT_ID:-nodeledge-mqtt-listener} # Matches topics on the form // (see README to widen it). MQTT_TOPIC_FILTER: ${MQTT_TOPIC_FILTER:-/+/+} MQTT_QOS: ${MQTT_QOS:-1} DB_DSN: postgresql://${POSTGRES_USER:-nodeledge}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in the stack environment}@db:5432/${POSTGRES_DB:-nodeledge} IMEI_MAP_FILE: /app/config/imei_map.json INJECT_IMEI_KEY: ${INJECT_IMEI_KEY:-imei} INJECT_DEVICE_KEY: ${INJECT_DEVICE_KEY:-device} # Comma-separated payload keys searched for a device timestamp. PAYLOAD_TIME_KEYS: ${PAYLOAD_TIME_KEYS:-timestamp,time,ts} LOG_LEVEL: ${LOG_LEVEL:-INFO} volumes: # IMEI -> device name mapping, see configs/listener/imei_map.json - ./configs/listener:/app/config:ro healthcheck: test: ["CMD", "python", "/app/listener/healthcheck.py"] interval: 30s timeout: 10s retries: 3 start_period: 30s api: build: context: . dockerfile: api/Dockerfile init: true restart: unless-stopped depends_on: db: condition: service_healthy environment: DB_DSN: postgresql://${POSTGRES_USER:-nodeledge}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in the stack environment}@db:5432/${POSTGRES_DB:-nodeledge} APP_TOKENS_FILE: /app/config/app_tokens.json # Read/write location for the listener's IMEI map, shared via the # ./configs/listener mount (the listener reads the same file) IMEI_MAP_FILE: /app/config-listener/imei_map.json API_MAX_LIMIT: ${API_MAX_LIMIT:-10000} LOG_LEVEL: ${LOG_LEVEL:-INFO} volumes: # app tokens for API auth + the listener's IMEI map; mounted rw so the # /configs endpoints can update them (writes are atomic) - ./configs/api:/app/config:rw - ./configs/listener:/app/config-listener:rw ports: - "${API_HOST_PORT:-18080}:8000" healthcheck: test: ["CMD", "python", "/app/api/healthcheck.py"] interval: 30s timeout: 10s retries: 3 start_period: 30s broker: # Optional MQTT broker for local testing; excluded unless the "test" # profile is active (docker compose --profile test ...). Point # MQTT_BROKER_HOST at your real broker in production. image: eclipse-mosquitto:2 profiles: ["test"] restart: unless-stopped ports: - "${MQTT_HOST_PORT:-1883}:1883" volumes: - ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro healthcheck: test: ["CMD", "mosquitto_pub", "-h", "127.0.0.1", "-t", "broker/healthcheck", "-m", "ping"] interval: 30s timeout: 5s retries: 3 volumes: db_data: