This commit is contained in:
Jonas Bengtsson
2026-09-16 06:08:37 +02:00
parent cce6a4960e
commit 8d2c54beae
54 changed files with 8480 additions and 41 deletions
+151
View File
@@ -0,0 +1,151 @@
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}
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}
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:
image: eclipse-mosquitto:2.1.2-alpine
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
networks:
- backend-net
# 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: dynsec denies anonymous clients, so we must connect as
# the seeded 'admin' (password from env, $$ = escaped for compose).
healthcheck:
test: ["CMD-SHELL", "mosquitto_sub -h 127.0.0.1 -u admin -P \"$$MOSQUITTO_DYNSEC_PASSWORD\" -t '$$SYS/broker/version' -C 1 -W 3 >/dev/null 2>&1"]
interval: 10s
timeout: 5s
retries: 12
start_period: 10s
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
volumes:
- backend_data:/data
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:
backend_data:
networks:
frontend-net:
backend-net: