L'iPhone n'est joignable depuis nulle part - CGNAT operateur en 4G, NAT de la box sur le WiFi du studio, UPnP indisponible. Le serveur RTSP de l'application camera est donc inutilisable de l'exterieur. Seule une connexion sortante traverse les deux NAT : c'est le telephone qui pousse. - MediaMTX en conteneur : SRT/RTMP en entree, WebRTC (WHEP) et HLS en sortie. La video ne traverse jamais la JVM - 800 kbps continus dans Spring Boot couteraient un thread par spectateur sans rien apporter. - SRT privilegie sur RTMP : RTMP est du TCP, un paquet perdu sur un lien mobile bloque tout le flux. SRT retransmet selectivement en UDP. - Lecture publique (choix assume), publication authentifiee : sans ca n'importe qui pousserait sa propre video sur le flux "biblio". - Console : client WHEP maison avec repli HLS automatique, et mise en pause quand l'onglet passe en arriere-plan. Correction d'un bug trouve en deployant : le @ExceptionHandler(Exception) avalait les exceptions que Spring sait deja traduire, si bien que toute URL inconnue renvoyait 500 au lieu de 404 - ce qui noyait les vraies pannes au milieu des 500 provoques par les crawlers. Test de regression ajoute. 35 tests au vert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
165 lines
6.3 KiB
YAML
165 lines
6.3 KiB
YAML
# =============================================================================
|
|
# 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
|
|
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}
|
|
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]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|