# Nodeledge MQTT listener stack (Portainer / docker compose compatible). # # Services: # db PostgreSQL holding the mqtt_messages table + the config # tables (app_tokens, imei_map, imei_attributes) # mqtt-listener subscribes to MQTT, injects IMEI + device into JSON payloads, writes rows # api FastAPI read API over the database, protected by app tokens # ui Vue management frontend (nginx) proxying /api to the api # 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} TZ: ${TZ:-Europe/Stockholm} 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} 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} TZ: ${TZ:-Europe/Stockholm} 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} # Seed token for the FIRST deployment: on the first authenticated # request the api inserts its digest into the (then empty) app_tokens # table. Rotate to real tokens via PUT /configs/app_tokens, then unset # this variable. Ignored once app_tokens has rows. BOOTSTRAP_APP_TOKEN: ${BOOTSTRAP_APP_TOKEN:-} BOOTSTRAP_APP_NAME: ${BOOTSTRAP_APP_NAME:-bootstrap} # First admin for the management UI: on the first login attempt the # api seeds this user (scrypt hash) into the empty users table. Set a # strong password, then unset after creating real users. BOOTSTRAP_ADMIN_USER: ${BOOTSTRAP_ADMIN_USER:-} BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD:-} API_MAX_LIMIT: ${API_MAX_LIMIT:-10000} # How long soft-deleted messages stay in retention (days) before # being purged. Env-configurable, no schema impact (expires_at is # computed per delete). RETENTION_DAYS: ${RETENTION_DAYS:-30} LOG_LEVEL: ${LOG_LEVEL:-INFO} TZ: ${TZ:-Europe/Stockholm} ports: - "${API_HOST_PORT:-18080}:8000" healthcheck: test: ["CMD", "python", "/app/api/healthcheck.py"] interval: 30s timeout: 10s retries: 3 start_period: 30s pgadmin: image: dpage/pgadmin4:9.17 restart: unless-stopped depends_on: db: condition: service_healthy environment: PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-change_this_bad_password} PGADMIN_DISABLE_POSTFIX: ${PGADMIN_DISABLE_POSTFIX} PGADMIN_LISTEN_ADDRESS: ${PGADMIN_LISTEN_ADDRESS} TZ: ${TZ:-Europe/Stockholm} volumes: - pgadmin_data:/var/lib/pgadmin ports: - ${PGADMIN_HOST_PORT:-18081}:80 healthcheck: test: ["CMD", "wget", "-O", "-", "http://localhost:80/misc/ping"] interval: 10s timeout: 10s start_period: 160s retries: 3 # The management UI (Vue SPA served by nginx). Kept separate from the # api container: the api stays a pure JSON API; the UI is independently # deployable and can be pointed at another API via API_UPSTREAM. ui: build: context: ./frontend dockerfile: Dockerfile restart: unless-stopped depends_on: api: condition: service_started environment: API_UPSTREAM: ${API_UPSTREAM:-api:8000} ports: - "${UI_HOST_PORT:-18090}:80" healthcheck: test: ["CMD", "wget", "-O", "-", "http://127.0.0.1:80/"] interval: 30s timeout: 10s retries: 3 start_period: 10s 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: pgadmin_data: