video-surveillance-app/docker-compose.yml
Adam Belkacemi d70892dce0 Flux video temps reel : l'iPhone pousse, MediaMTX restitue
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>
2026-09-12 18:08:34 +02:00

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