2026-09-12 17:32:45 +02:00
|
|
|
# =============================================================================
|
|
|
|
|
# iot-alarm-system
|
|
|
|
|
#
|
|
|
|
|
# Deux reseaux :
|
|
|
|
|
# - "default" : prive, Kafka et PostgreSQL n'y sont joignables que par l'app.
|
|
|
|
|
# Aucun port n'est publie sur l'hote - le broker n'est pas expose a Internet.
|
|
|
|
|
# - "edge" : reseau externe partage, ou le reverse proxy Caddy du serveur
|
|
|
|
|
# vient chercher l'application pour la servir en HTTPS.
|
|
|
|
|
#
|
|
|
|
|
# Note sur Zookeeper : absent volontairement. Kafka tourne en mode KRaft, son
|
|
|
|
|
# quorum interne. Zookeeper est deprecie depuis Kafka 3.5 et supprime en 4.0 ;
|
|
|
|
|
# le garder ajouterait un conteneur, ~400 Mo de RAM et une piece a maintenir.
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
services:
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Bus d'evenements - Kafka en KRaft mono-noeud
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
kafka:
|
|
|
|
|
image: apache/kafka:3.9.1
|
|
|
|
|
container_name: iot-alarm-kafka
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
environment:
|
|
|
|
|
CLUSTER_ID: "iot-alarm-cluster-0001"
|
|
|
|
|
KAFKA_NODE_ID: 1
|
|
|
|
|
KAFKA_PROCESS_ROLES: broker,controller
|
|
|
|
|
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
|
|
|
|
|
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
|
|
|
|
|
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
|
|
|
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
|
|
|
|
|
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
|
|
|
|
|
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
|
|
|
|
# Mono-noeud : aucune replication possible, on l'assume explicitement.
|
|
|
|
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
|
|
|
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
|
|
|
|
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
|
|
|
|
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
|
|
|
|
|
KAFKA_LOG_RETENTION_HOURS: 72
|
|
|
|
|
KAFKA_LOG_DIRS: /var/lib/kafka/data
|
|
|
|
|
KAFKA_HEAP_OPTS: "-Xmx400m -Xms256m"
|
|
|
|
|
volumes:
|
|
|
|
|
- kafka_data:/var/lib/kafka/data
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD-SHELL", "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 >/dev/null 2>&1"]
|
|
|
|
|
interval: 15s
|
|
|
|
|
timeout: 10s
|
|
|
|
|
retries: 12
|
|
|
|
|
start_period: 40s
|
|
|
|
|
mem_limit: 800m
|
|
|
|
|
networks: [default]
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Persistance - write model et read model
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
postgres:
|
|
|
|
|
image: postgres:16-alpine
|
|
|
|
|
container_name: iot-alarm-postgres
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
environment:
|
|
|
|
|
POSTGRES_DB: iotalarm
|
|
|
|
|
POSTGRES_USER: iotalarm
|
|
|
|
|
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD est obligatoire, voir .env.example}
|
|
|
|
|
# Le checksum des pages detecte une corruption disque silencieuse.
|
|
|
|
|
POSTGRES_INITDB_ARGS: "--data-checksums"
|
|
|
|
|
volumes:
|
|
|
|
|
- postgres_data:/var/lib/postgresql/data
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD-SHELL", "pg_isready -U iotalarm -d iotalarm"]
|
|
|
|
|
interval: 10s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 10
|
|
|
|
|
start_period: 15s
|
|
|
|
|
mem_limit: 256m
|
|
|
|
|
networks: [default]
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Application Spring Boot
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
app:
|
|
|
|
|
build:
|
|
|
|
|
context: .
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
image: iot-alarm-system:1.0.0
|
|
|
|
|
container_name: iot-alarm-app
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
depends_on:
|
|
|
|
|
kafka:
|
|
|
|
|
condition: service_healthy
|
|
|
|
|
postgres:
|
|
|
|
|
condition: service_healthy
|
Sonde de presence video : la camera muette devient un evenement capteur
Le domaine connaissait deja CAMERA_OFFLINE, classe CRITICAL, mais rien ne
l'emettait. CameraLivenessProbe interroge l'API du serveur media et comble
ce trou.
Elle passe par le meme port IngestSensorEventUseCase que l'endpoint REST
et le simulateur : trois producteurs de natures tres differentes, un seul
contrat d'entree, et le meme trajet Kafka -> agregat ensuite. Si l'alarme
est desarmee, une coupure video ne reveille personne - c'est l'agregat qui
tranche, pas la sonde.
Le seuil est la partie qui compte. Sur un lien 4G le flux tombe
regulierement sans que personne n'ait touche au telephone : tunnel,
bascule d'antenne, congestion. Alerter des la premiere sonde ratee
produirait plusieurs CRITICAL par jour, et on apprend vite a ignorer des
alertes qui crient pour rien - y compris le jour ou elles ont raison.
Trois sondes consecutives, soit une minute d'absence reelle. Six tests
couvrent ce comportement, dont la remise a zero du compteur au retour du
flux.
Utilisateur d'API dedie cote MediaMTX, restreint au reseau Docker interne.
41 tests au vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 18:24:45 +02:00
|
|
|
# Demarrage seulement : la sonde tolere un serveur media absent, elle
|
|
|
|
|
# ne doit pas empecher l'alarme de fonctionner.
|
|
|
|
|
mediamtx:
|
|
|
|
|
condition: service_started
|
2026-09-12 17:32:45 +02:00
|
|
|
environment:
|
|
|
|
|
SPRING_PROFILES_ACTIVE: prod
|
|
|
|
|
KAFKA_BOOTSTRAP_SERVERS: kafka:9092
|
|
|
|
|
DB_URL: jdbc:postgresql://postgres:5432/iotalarm
|
|
|
|
|
DB_USERNAME: iotalarm
|
|
|
|
|
DB_PASSWORD: ${DB_PASSWORD}
|
|
|
|
|
SIMULATOR_ENABLED: ${SIMULATOR_ENABLED:-true}
|
|
|
|
|
SIMULATOR_INTERVAL: ${SIMULATOR_INTERVAL:-8s}
|
|
|
|
|
ALARM_EXIT_DELAY: ${ALARM_EXIT_DELAY:-30s}
|
|
|
|
|
ALARM_MIN_CONFIDENCE: ${ALARM_MIN_CONFIDENCE:-60}
|
Sonde de presence video : la camera muette devient un evenement capteur
Le domaine connaissait deja CAMERA_OFFLINE, classe CRITICAL, mais rien ne
l'emettait. CameraLivenessProbe interroge l'API du serveur media et comble
ce trou.
Elle passe par le meme port IngestSensorEventUseCase que l'endpoint REST
et le simulateur : trois producteurs de natures tres differentes, un seul
contrat d'entree, et le meme trajet Kafka -> agregat ensuite. Si l'alarme
est desarmee, une coupure video ne reveille personne - c'est l'agregat qui
tranche, pas la sonde.
Le seuil est la partie qui compte. Sur un lien 4G le flux tombe
regulierement sans que personne n'ait touche au telephone : tunnel,
bascule d'antenne, congestion. Alerter des la premiere sonde ratee
produirait plusieurs CRITICAL par jour, et on apprend vite a ignorer des
alertes qui crient pour rien - y compris le jour ou elles ont raison.
Trois sondes consecutives, soit une minute d'absence reelle. Six tests
couvrent ce comportement, dont la remise a zero du compteur au retour du
flux.
Utilisateur d'API dedie cote MediaMTX, restreint au reseau Docker interne.
41 tests au vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 18:24:45 +02:00
|
|
|
MEDIA_API_URL: http://mediamtx:9997
|
|
|
|
|
MEDIA_API_PASSWORD: ${MEDIA_API_PASSWORD}
|
|
|
|
|
CAMERA_PROBE_ENABLED: ${CAMERA_PROBE_ENABLED:-true}
|
2026-09-12 17:32:45 +02:00
|
|
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
|
|
|
|
mem_limit: 512m
|
|
|
|
|
# Joint les deux reseaux : "default" pour parler a Kafka et PostgreSQL,
|
|
|
|
|
# "edge" pour etre joignable par le reverse proxy.
|
|
|
|
|
networks: [default, edge]
|
|
|
|
|
|
2026-09-12 18:08:34 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Serveur media - ingestion du flux iPhone, restitution navigateur
|
|
|
|
|
#
|
|
|
|
|
# L'iPhone POUSSE vers ce service (SRT ou RTMP). Il ne peut pas etre
|
|
|
|
|
# interroge : en 4G comme derriere la box du studio, il n'a pas d'adresse
|
|
|
|
|
# joignable depuis Internet. Seul un flux sortant traverse le CGNAT.
|
|
|
|
|
#
|
|
|
|
|
# Les ports d'ingestion sont publies sur l'hote, ce sont les seuls du projet
|
|
|
|
|
# a l'etre. Les sorties HTTP (HLS 8888, WebRTC 8889) restent internes et
|
|
|
|
|
# passent par Caddy, qui leur apporte le TLS du domaine.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
mediamtx:
|
|
|
|
|
image: bluenviron/mediamtx:1.21.0
|
|
|
|
|
container_name: iot-alarm-mediamtx
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
volumes:
|
|
|
|
|
- ./deploy/mediamtx/mediamtx.yml:/mediamtx.yml:ro
|
|
|
|
|
ports:
|
|
|
|
|
- "1935:1935" # RTMP (TCP) - ingestion, repli
|
|
|
|
|
- "8890:8890/udp" # SRT (UDP) - ingestion, a privilegier
|
|
|
|
|
- "8189:8189/udp" # WebRTC ICE - media vers les navigateurs
|
|
|
|
|
mem_limit: 256m
|
|
|
|
|
# Image sans shell (base scratch) : pas de healthcheck par commande.
|
|
|
|
|
# La disponibilite se lit sur /actuator du cote applicatif et dans les
|
|
|
|
|
# logs du conteneur.
|
|
|
|
|
networks: [default, edge]
|
|
|
|
|
|
2026-09-12 17:32:45 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Nginx - profil "standalone" uniquement
|
|
|
|
|
#
|
|
|
|
|
# Sur le serveur de production, le frontal est le Caddy deja en place (il
|
|
|
|
|
# detient 80/443 et sert cinq autres domaines) : voir deploy/Caddyfile.snippet.
|
|
|
|
|
# Ce Nginx sert au cas "je clone le depot et je lance tout sur ma machine" :
|
|
|
|
|
# docker compose --profile standalone up -d -> http://localhost:8081
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
nginx:
|
|
|
|
|
image: nginx:alpine
|
|
|
|
|
container_name: iot-alarm-nginx
|
|
|
|
|
profiles: [standalone]
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
depends_on: [app]
|
|
|
|
|
ports:
|
|
|
|
|
- "8081:80"
|
|
|
|
|
volumes:
|
|
|
|
|
- ./deploy/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
|
|
|
|
mem_limit: 64m
|
|
|
|
|
networks: [default]
|
|
|
|
|
|
|
|
|
|
volumes:
|
|
|
|
|
kafka_data:
|
|
|
|
|
postgres_data:
|
|
|
|
|
|
|
|
|
|
networks:
|
|
|
|
|
default:
|
|
|
|
|
name: iot-alarm-internal
|
|
|
|
|
edge:
|
|
|
|
|
external: true
|
|
|
|
|
name: edge
|