177 lines
6.7 KiB
YAML
177 lines
6.7 KiB
YAML
name: mqtt-platform
|
|
|
|
# MQTT Management Platform — Release 1
|
|
# See specification.md §4, §5, §29, §60 for architecture decisions.
|
|
|
|
x-backend-env: &backend-env
|
|
APP_ENV: ${APP_ENV:-development}
|
|
DATABASE_URL: postgresql+psycopg://${DATABASE_USER:-mqttadmin}:${DATABASE_PASSWORD:-changeme}@${DATABASE_HOST:-mqtt-db}:${DATABASE_PORT:-5432}/${DATABASE_NAME:-mqttadmin}
|
|
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET must be set}
|
|
SECRET_ENCRYPTION_KEY: ${SECRET_ENCRYPTION_KEY:?SECRET_ENCRYPTION_KEY must be set}
|
|
ADMIN_INITIAL_USERNAME: ${ADMIN_INITIAL_USERNAME:-admin}
|
|
ADMIN_INITIAL_PASSWORD: ${ADMIN_INITIAL_PASSWORD:?ADMIN_INITIAL_PASSWORD must be set}
|
|
MOSQUITTO_HOST: ${MOSQUITTO_HOST:-mosquitto}
|
|
MOSQUITTO_MGMT_PORT: ${MOSQUITTO_MGMT_PORT:-1883}
|
|
MOSQUITTO_DYNSEC_USERNAME: ${MOSQUITTO_DYNSEC_USERNAME:-admin}
|
|
MOSQUITTO_DYNSEC_PASSWORD: ${MOSQUITTO_DYNSEC_PASSWORD:?MOSQUITTO_DYNSEC_PASSWORD must be set}
|
|
CONTROL_API_TOKEN: ${CONTROL_API_TOKEN:?CONTROL_API_TOKEN must be set}
|
|
DEFAULT_TLS_MIN_VERSION: ${DEFAULT_TLS_MIN_VERSION:-tlsv1.2}
|
|
DEFAULT_CERT_VALIDITY_DAYS: ${DEFAULT_CERT_VALIDITY_DAYS:-365}
|
|
DEFAULT_CA_VALIDITY_DAYS: ${DEFAULT_CA_VALIDITY_DAYS:-3650}
|
|
# Broker container ports published to the host (compose `ports:`);
|
|
# keep in sync when editing the mosquitto port mappings.
|
|
PUBLISHED_PORTS: ${PUBLISHED_PORTS:-1883,8883}
|
|
|
|
services:
|
|
mqtt-db:
|
|
image: postgres:17-alpine
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: ${DATABASE_USER:-mqttadmin}
|
|
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-changeme}
|
|
POSTGRES_DB: ${DATABASE_NAME:-mqttadmin}
|
|
volumes:
|
|
- mqtt_db_data:/var/lib/postgresql/data
|
|
# Port deliberately NOT published to the host (spec §5.2).
|
|
networks:
|
|
- backend-net
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER:-mqttadmin} -d ${DATABASE_NAME:-mqttadmin}"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 12
|
|
|
|
mosquitto:
|
|
# Broker image = pinned base (eclipse-mosquitto:2.1.2-alpine, pinned
|
|
# inside mosquitto/Dockerfile) with our bootstrap config BAKED IN.
|
|
# Docker's copy-up then seeds a fresh mosquitto_config volume with
|
|
# OUR dynsec-enabled config instead of the stock anonymous one;
|
|
# existing volumes (holding deployed revisions) are never touched.
|
|
build:
|
|
context: ./mosquitto
|
|
restart: unless-stopped
|
|
# Spec §29: no docker.sock. Control sidecar shares the broker's PID namespace
|
|
# and the mosquitto_config volume; it signals PID 1 (the broker) directly.
|
|
environment:
|
|
# Used by the dynsec plugin on first start to seed the 'admin' client
|
|
# (verified: Mosquitto 2.1 dynamic security docs).
|
|
MOSQUITTO_DYNSEC_PASSWORD: ${MOSQUITTO_DYNSEC_PASSWORD:?MOSQUITTO_DYNSEC_PASSWORD must be set}
|
|
volumes:
|
|
- mosquitto_config:/mosquitto/config
|
|
- mosquitto_data:/mosquitto/data
|
|
- mosquitto_log:/mosquitto/log
|
|
# Shared PKI material (spec §6): backend writes decrypted cert/key
|
|
# files here before a TLS config deploy; the broker reads them.
|
|
- mosquitto_certs:/mosquitto/certs
|
|
networks:
|
|
- backend-net
|
|
ports:
|
|
- "${MQTT_PORT:-1883}:1883"
|
|
- "${MQTTS_PORT:-8883}:8883"
|
|
# Release 1 ships an internal-only, plaintext listener on the backend
|
|
# network for the management channel; production listeners are added
|
|
# through the platform (spec §60). Nothing is published to the host.
|
|
# Healthcheck: a REAL dynamic-security roundtrip via mosquitto_ctrl —
|
|
# proves broker up + dynsec plugin loaded + admin auth works, exactly
|
|
# what the backend depends on. NOT a $SYS probe: the 2.1 auto-generated
|
|
# admin role set does not guarantee $SYS read access.
|
|
# ($$ escapes compose interpolation; resolved inside the container.)
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "mosquitto_ctrl -h 127.0.0.1 -u admin -P \"$$MOSQUITTO_DYNSEC_PASSWORD\" dynsec listClients >/dev/null 2>&1"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 15s
|
|
|
|
mosquitto-control:
|
|
build:
|
|
context: ./broker-control
|
|
restart: unless-stopped
|
|
depends_on:
|
|
mosquitto:
|
|
condition: service_started
|
|
# Shares PID namespace with the broker so `mosquitto_signal -p 1 ...`
|
|
# reaches the broker process without mounting docker.sock.
|
|
pid: "service:mosquitto"
|
|
environment:
|
|
# Minimal default token for the internal control API; in production
|
|
# this MUST be replaced (it only guards a backend-net-only endpoint).
|
|
CONTROL_API_TOKEN: ${CONTROL_API_TOKEN:?CONTROL_API_TOKEN must be set}
|
|
# Dynsec admin credentials for the post-deploy broker health probe
|
|
# (mosquitto_sub against $SYS/broker/version on loopback).
|
|
MOSQUITTO_DYNSEC_USERNAME: ${MOSQUITTO_DYNSEC_USERNAME:-admin}
|
|
MOSQUITTO_DYNSEC_PASSWORD: ${MOSQUITTO_DYNSEC_PASSWORD:?MOSQUITTO_DYNSEC_PASSWORD must be set}
|
|
MOSQUITTO_CONFIG_DIR: /mosquitto/config
|
|
volumes:
|
|
- mosquitto_config:/mosquitto/config
|
|
networks:
|
|
- backend-net
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8090/health/live"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 12
|
|
start_period: 5s
|
|
|
|
mqtt-backend:
|
|
build:
|
|
context: ./backend
|
|
restart: unless-stopped
|
|
depends_on:
|
|
mqtt-db:
|
|
condition: service_healthy
|
|
mosquitto:
|
|
condition: service_healthy
|
|
mosquitto-control:
|
|
condition: service_healthy
|
|
environment:
|
|
<<: *backend-env
|
|
# Where the backend publishes cert/key files for the broker (spec §6).
|
|
MOSQUITTO_CERTS_DIR: /mosquitto/certs
|
|
volumes:
|
|
- backend_data:/data
|
|
# Same volume the broker mounts at /mosquitto/certs — the deploy
|
|
# pipeline writes decrypted PEM files here (spec §29: files, not
|
|
# sockets).
|
|
- mosquitto_certs:/mosquitto/certs
|
|
networks:
|
|
- frontend-net
|
|
- backend-net
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/live', timeout=3)\""]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 15s
|
|
|
|
mqtt-frontend:
|
|
build:
|
|
context: ./frontend
|
|
args:
|
|
# Only non-secret build args cross the frontend boundary (spec §7.2).
|
|
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-/api/v1}
|
|
restart: unless-stopped
|
|
depends_on:
|
|
mqtt-backend:
|
|
condition: service_healthy
|
|
networks:
|
|
- frontend-net
|
|
ports:
|
|
- "${FRONTEND_PORT:-8080}:80"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:80/ || exit 1"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 12
|
|
|
|
volumes:
|
|
mqtt_db_data:
|
|
mosquitto_config:
|
|
mosquitto_data:
|
|
mosquitto_log:
|
|
mosquitto_certs:
|
|
backend_data:
|
|
|
|
networks:
|
|
frontend-net:
|
|
backend-net: |