Add serve-webapp.sh to manage the webapp stack on the lightweight machine

Single command to start the interactive map container in the background
(auto-restart after reboot), with stop/restart/status/sync/logs/update
subcommands. Configuration lives in webapp.env (template provided,
git-ignored): remote generation URL, shared token, rsync cache command,
periodic sync and allowed network. The SSH key is mounted automatically
for rsync, port conflicts are detected before launch, and update pulls the
repo, rebuilds the image and restarts.

Deploy doc updated: the script is now the recommended install path on the
Raspberry Pi, alongside run.sh (foreground) and docker compose.
This commit is contained in:
Antoine Jacquin
2026-09-04 21:48:35 +02:00
parent 23969c9e14
commit 8e1087b12b
5 changed files with 251 additions and 15 deletions

1
.gitignore vendored
View File

@ -42,6 +42,7 @@ htmlcov/
# Fichiers de configuration locaux
.env
.env.local
webapp.env
# Données et artefacts internes (jamais dans le dépôt)
data/

View File

@ -4,6 +4,7 @@
- build: `docker build -t lidar-lidar .`
- build webapp légère (Raspberry Pi, déploiement 2 machines — cf. `docs/DEPLOY_WEBAPP.md`): `docker compose -f docker-compose.webapp.yml up -d --build` (image `Dockerfile.webapp`, sans PDAL/GPU)
- build générateur de tuiles (machine de traitement): `docker compose -f docker-compose.worker.yml up -d --build` (service `worker`, API pour les webapp distantes)
- stack webapp (machine légère): `./serve-webapp.sh [start|stop|restart|status|sync|logs|update]`, config dans `webapp.env` (modèle `webapp.env.example`, ignoré par git)
- test all: `./run.sh --test` (rebuild automatique de l'image avant les tests ; en `docker run` direct, rebuild manuellement d'abord)
- test file: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.<module>`
- test case: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.<module>::<TestClass>::<test_method>`

View File

@ -88,17 +88,32 @@ Sur la machine puissante, le dossier `output/` doit être lisible par ce
compte (ex. `/srv/lidar/output` si vous préférez un chemin dédié — adaptez
LIDAR_SYNC_CMD).
Avec `run.sh`, `~/.ssh` (clé + known_hosts) est monté automatiquement en
lecture seule dans le conteneur ; avec `docker compose`, le montage
équivalent est à décommenter dans `docker-compose.webapp.yml` (voir
l'option b ci-dessous).
Avec `serve-webapp.sh` et `run.sh`, `~/.ssh` (clé + known_hosts) est monté
automatiquement en lecture seule dans le conteneur ; avec `docker compose`,
le montage équivalent est à décommenter dans `docker-compose.webapp.yml`
(voir l'option c ci-dessous).
### 3. Configurer et lancer
Deux façons, au choix :
Trois façons, au choix :
**a. `run.sh` (le plus simple)** — conteneur webapp seul, cache local des
tuiles ; les variables se passent depuis l'environnement hôte, `~/.ssh`
**a. `serve-webapp.sh` (recommandé)** — script de gestion de la stack
(arrière-plan, redémarrage automatique au reboot, sous-commandes). La
configuration vit dans `webapp.env` (copie du modèle, non versionné) :
```bash
cp webapp.env.example webapp.env
nano webapp.env # LIDAR_GENERATION_URL, LIDAR_SYNC_CMD, jeton...
./serve-webapp.sh # démarre (build au premier lancement) et attend le serveur
```
Sous-commandes : `stop`, `restart` (relit `webapp.env`), `status` (conteneur
+ état de la sync), `sync` (force rsync + vignettes), `logs`, `update`
(`git pull` + rebuild + redémarrage). Port hôte via `WEBAPP_PORT` dans
`webapp.env` ou l'environnement (`WEBAPP_PORT=9000 ./serve-webapp.sh`).
**b. `run.sh`** — conteneur au premier plan (Ctrl-C arrête tout) : pratique
pour tester, les variables se passent depuis l'environnement hôte, `~/.ssh`
est monté automatiquement :
```bash
@ -124,7 +139,7 @@ DNAT Docker pour les clients du LAN) ; derrière un reverse proxy local
Les autres clients consultent la carte normalement : les boutons de
génération sont masqués et l'API répond 403.
**b. `docker compose`** — éditer `docker-compose.webapp.yml` :
**c. `docker compose`** — éditer `docker-compose.webapp.yml` :
- `LIDAR_GENERATION_URL` : `http://<ip-machine>:8973`
- `LIDAR_REMOTE_TOKEN` : la valeur de `LIDAR_API_TOKEN` de la machine
@ -158,8 +173,9 @@ docker compose -f docker-compose.webapp.yml up -d --build
La carte est sur `http://<ip-pi>:8973/`.
`run.sh` lance le conteneur au premier plan (Ctrl-C arrête tout) : pratique
pour tester. Pour une installation permanente, préférer `docker compose`
(`restart: unless-stopped` : la carte revient après un reboot du Pi).
pour tester. Pour une installation permanente, `serve-webapp.sh` (option a)
ou `docker compose` (option c) redémarrent la carte après un reboot du Pi
(`--restart unless-stopped`).
### 4. Premier chargement
@ -174,7 +190,14 @@ false`.) Le bouton ↻ de la carte fait la même chose (sync + vignettes).
## Mise à jour
Sur le Pi, après un `git pull` (ou rsync du code) :
Avec `serve-webapp.sh`, une seule commande (`git pull`, rebuild de l'image,
redémarrage) :
```bash
./serve-webapp.sh update
```
Sinon, sur le Pi, après un `git pull` (ou rsync du code) :
```bash
docker compose -f docker-compose.webapp.yml up -d --build # rebuild + redémarrage
@ -203,10 +226,13 @@ chaque lancement ; il suffit de relancer la commande. Le cache de tuiles
`LIDAR_GENERATION_URL` (IP + port 8973). La consultation de la carte
reste possible avec le cache local.
- **Carte vide après sync** : attendre la fin du rebuild (`GET /api/sync` →
`"running": false`) puis recharger la page ; le bouton ↻ de la carte
enchaîne sync + vignettes automatiquement.
- **Port 8973 déjà pris** : `--serve-webapp 9000` avec `run.sh`, ou changer
le mapping `ports:` en compose (le conteneur écoute toujours sur 8973).
`"running": false`, ou `./serve-webapp.sh status`) puis recharger la
page ; le bouton ↻ de la carte enchaîne sync + vignettes
automatiquement.
- **Port 8973 déjà pris** : `WEBAPP_PORT=9000 ./serve-webapp.sh` (ou la
variable dans `webapp.env`), `--serve-webapp 9000` avec `run.sh`, ou
changer le mapping `ports:` en compose (le conteneur écoute toujours sur
8973).
## Fonctionnement
@ -244,3 +270,6 @@ chaque lancement ; il suffit de relancer la commande. Le cache de tuiles
- `docker-compose.worker.yml` : générateur de tuiles (machine puissante).
- `Dockerfile.webapp`, `docker-compose.webapp.yml` : webapp légère ARM64
(FastAPI + Pillow AVIF natif + pyproj, ~200 Mo).
- `serve-webapp.sh`, `webapp.env.example` : gestion de la stack webapp sur
le Pi (démarrage/arrêt/sync/logs/update), configuration locale dans
`webapp.env` (ignoré par git).

180
serve-webapp.sh Executable file
View File

@ -0,0 +1,180 @@
#!/bin/bash
# Stack webapp LiDAR — gestion du conteneur carte interactive sur la machine
# légère (Raspberry Pi). Cf. docs/DEPLOY_WEBAPP.md.
#
# Configuration : variables dans webapp.env (modèle : webapp.env.example,
# non versionné) ou exportées dans l'environnement :
# WEBAPP_PORT port hôte (défaut 8973)
# LIDAR_GENERATION_URL machine de traitement (ex. http://192.168.1.50:8973)
# LIDAR_REMOTE_TOKEN jeton si la machine exige LIDAR_API_TOKEN
# LIDAR_SYNC_CMD rsync qui remplit le cache local de tuiles
# LIDAR_AUTO_SYNC_SECONDS resynchronisation périodique (défaut 0 = off)
# LIDAR_REGEN_CIDR réseau autorisé à lancer les générations
# (défaut 192.168.1.0/24, vide = restriction levée)
#
# Usage :
# ./serve-webapp.sh [start] démarrer (arrière-plan, redémarrage auto)
# ./serve-webapp.sh stop arrêter et supprimer le conteneur
# ./serve-webapp.sh restart redémarrer (relit webapp.env)
# ./serve-webapp.sh status état du conteneur et de la dernière sync
# ./serve-webapp.sh sync forcer sync rsync + régénération des vignettes
# ./serve-webapp.sh logs suivre les logs du conteneur
# ./serve-webapp.sh update git pull, rebuild de l'image, redémarrer
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
IMAGE="lidar-webapp"
CONTAINER="lidar-webapp"
WEBAPP_PORT="${WEBAPP_PORT:-8973}"
BASE_URL="http://127.0.0.1:${WEBAPP_PORT}"
# Configuration locale (non versionnée) : webapp.env surcharge l'environnement
if [ -f "${SCRIPT_DIR}/webapp.env" ]; then
set -a
# shellcheck disable=SC1091
. "${SCRIPT_DIR}/webapp.env"
set +a
fi
CMD="${1:-start}"
build_image() {
echo "Build de l'image webapp (cache Docker)..."
if ! docker build -f "${SCRIPT_DIR}/Dockerfile.webapp" -t "$IMAGE" "$SCRIPT_DIR" > /tmp/lidar_webapp_build.log 2>&1; then
cat /tmp/lidar_webapp_build.log >&2
echo "Échec du build (journal : /tmp/lidar_webapp_build.log)" >&2
exit 1
fi
}
lan_ip() {
hostname -I 2> /dev/null | awk '{print $1}'
}
cmd_start() {
mkdir -p "${SCRIPT_DIR}/output"
if curl -fsS "${BASE_URL}/api/status" > /dev/null 2>&1; then
echo "Le port ${WEBAPP_PORT} répond déjà (webapp déjà lancée ?)." >&2
echo "Arrêter l'existant, ou relancer avec un autre port : WEBAPP_PORT=9000 $0" >&2
exit 1
fi
if ! docker image inspect "$IMAGE" > /dev/null 2>&1; then
build_image
fi
ENV_ARGS=()
if [ -n "${LIDAR_GENERATION_URL:-}" ]; then
ENV_ARGS+=(-e LIDAR_GENERATION_URL="$LIDAR_GENERATION_URL")
fi
if [ -n "${LIDAR_REMOTE_TOKEN:-}" ]; then
ENV_ARGS+=(-e LIDAR_REMOTE_TOKEN="$LIDAR_REMOTE_TOKEN")
fi
if [ -n "${LIDAR_SYNC_CMD:-}" ]; then
ENV_ARGS+=(-e LIDAR_SYNC_CMD="$LIDAR_SYNC_CMD")
fi
if [ -n "${LIDAR_AUTO_SYNC_SECONDS:-}" ]; then
ENV_ARGS+=(-e LIDAR_AUTO_SYNC_SECONDS="$LIDAR_AUTO_SYNC_SECONDS")
fi
# Définie même vide (= restriction levée) ; absente = défaut webapp.py
if [ -n "${LIDAR_REGEN_CIDR+x}" ]; then
ENV_ARGS+=(-e LIDAR_REGEN_CIDR="${LIDAR_REGEN_CIDR:-}")
fi
# Clé SSH du rsync (LIDAR_SYNC_CMD) : montée en lecture seule si présente
SSH_ARGS=()
if [ -d "${HOME}/.ssh" ]; then
SSH_ARGS+=(-v "${HOME}/.ssh:/home/lidar/.ssh:ro")
fi
docker rm -f "$CONTAINER" > /dev/null 2>&1 || true
docker run -d --init \
--name "$CONTAINER" \
--restart unless-stopped \
--user 1000:1000 \
-p "${WEBAPP_PORT}:8973" \
-v "${SCRIPT_DIR}/output:/data/output" \
"${SSH_ARGS[@]}" \
-e LIDAR_OUTPUT_DIR=/data/output \
"${ENV_ARGS[@]}" \
"$IMAGE" > /dev/null
echo -n "Attente du serveur sur le port ${WEBAPP_PORT}..."
UP=0
for _ in $(seq 1 30); do
if curl -fsS "${BASE_URL}/api/status" > /dev/null 2>&1; then
UP=1
break
fi
sleep 1
done
if [ "$UP" != "1" ]; then
echo " KO (voir ./serve-webapp.sh logs)" >&2
exit 1
fi
echo " OK"
echo "============================================"
echo " Carte LiDAR — webapp (${CONTAINER})"
echo "============================================"
echo " URL : ${BASE_URL}/"
if [ -n "$(lan_ip)" ]; then
echo " URL (LAN) : http://$(lan_ip):${WEBAPP_PORT}/"
fi
echo " Génération distante: ${LIDAR_GENERATION_URL:-non configurée}"
if [ -n "${LIDAR_SYNC_CMD:-}" ]; then
if [ -n "${LIDAR_AUTO_SYNC_SECONDS:-}" ] && [ "${LIDAR_AUTO_SYNC_SECONDS}" != "0" ]; then
echo " Cache local tuiles : rsync (auto toutes les ${LIDAR_AUTO_SYNC_SECONDS}s)"
else
echo " Cache local tuiles : rsync (à la demande)"
fi
else
echo " Cache local tuiles : désactivé (LIDAR_SYNC_CMD)"
fi
echo "============================================"
}
cmd_stop() {
docker rm -f "$CONTAINER" > /dev/null 2>&1 || true
echo "Conteneur ${CONTAINER} arrêté."
}
cmd_status() {
if docker ps --format '{{.Names}}' 2> /dev/null | grep -qx "$CONTAINER"; then
echo "Conteneur : ${CONTAINER} en cours d'exécution ($(docker ps --filter name="^${CONTAINER}$" --format '{{.Status}}'))"
else
echo "Conteneur : ${CONTAINER} arrêté"
fi
if curl -fsS "${BASE_URL}/api/sync" 2> /dev/null; then
echo ""
else
echo "Webapp : injoignable sur ${BASE_URL}"
fi
}
cmd_sync() {
CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST "${BASE_URL}/api/sync" || true)"
case "$CODE" in
200) echo "Sync lancée (suivi : ./serve-webapp.sh status, logs : ./serve-webapp.sh logs)" ;;
409) echo "Une sync est déjà en cours (suivi : ./serve-webapp.sh status)" ;;
*)
echo "Échec : HTTP ${CODE:-pas de réponse} (webapp injoignable sur ${BASE_URL} ?)" >&2
exit 1
;;
esac
}
cmd_update() {
echo "Mise à jour du code (git pull)..."
git -C "$SCRIPT_DIR" pull --ff-only
build_image
cmd_start
}
case "$CMD" in
start) cmd_start ;;
stop) cmd_stop ;;
restart) cmd_stop; cmd_start ;;
status) cmd_status ;;
sync) cmd_sync ;;
logs) exec docker logs -f "$CONTAINER" ;;
update) cmd_update ;;
*)
echo "Usage : $0 [start|stop|restart|status|sync|logs|update]" >&2
exit 1
;;
esac

25
webapp.env.example Normal file
View File

@ -0,0 +1,25 @@
# Configuration de la stack webapp — copier en webapp.env et adapter.
# webapp.env est ignoré par git (jetons, chemins locaux).
# Syntaxe shell simple : KEY=valeur (les valeurs avec espaces entre guillemets).
# Port hôte de la carte (défaut 8973)
#WEBAPP_PORT=8973
# Machine de traitement (service worker de docker-compose.worker.yml).
# Commenté : consultation autonome du cache local, aucune génération.
#LIDAR_GENERATION_URL=http://192.168.1.50:8973
# Jeton partagé si la machine de traitement définit LIDAR_API_TOKEN
#LIDAR_REMOTE_TOKEN=un-secret-à-partager
# rsync qui ramène les tuiles traitées vers le cache local.
# Les vignettes (index_thumbs/, index_subtiles/) sont régénérées localement,
# ne pas les synchroniser.
#LIDAR_SYNC_CMD="rsync -a --delete --exclude=*.tif --exclude=.generation* --exclude=index_thumbs --exclude=index_subtiles lidar@192.168.1.50:/srv/lidar/output/ /data/output/"
# Cache auto-entretenu : resync + vignettes toutes les N secondes (0 = off)
#LIDAR_AUTO_SYNC_SECONDS=600
# Réseau autorisé à lancer les générations depuis la carte
# (défaut 192.168.1.0/24 ; vide pour lever la restriction)
#LIDAR_REGEN_CIDR=192.168.1.0/24