Docker : base SQLite en bind mount (accès direct sur l'hôte)

Remplace le volume nommé Docker (trajets-data) par un bind mount ./data:/data,
afin d'accéder au fichier de base sans entrer dans le conteneur (inspection,
sauvegarde).

- docker-compose.yml : volumes backend -> ./data:/data ; déclaration du volume
  nommé retirée.
- .gitignore / .dockerignore : exclusion du dossier data/ (base jamais versionnée
  ni envoyée au contexte de build).
- design.md : références bind mount + nouvelle section « Accès à la base de
  données » (inspection, sauvegarde à froid, migration depuis l'ancien volume).
- run-docker.sh supprimé : script obsolète (ancienne archi, contexte de build et
  port périmés) et redondant avec docker compose v2 désormais disponible.

Déploiement validé : docker compose up -d --build fonctionne, base montée depuis
l'hôte.
This commit is contained in:
2026-09-20 19:31:49 +02:00
parent 259d9e16a2
commit e84c2356f1
5 changed files with 49 additions and 105 deletions
+1
View File
@@ -15,6 +15,7 @@ frontend/.angular
backend/SchotzIadKmTracker.Tests backend/SchotzIadKmTracker.Tests
# --- Bases SQLite locales (la base vit sur le volume /data) --- # --- Bases SQLite locales (la base vit sur le volume /data) ---
data/
**/*.db **/*.db
**/*.db-shm **/*.db-shm
**/*.db-wal **/*.db-wal
+2
View File
@@ -18,6 +18,8 @@ backend/**/obj/
# --- Bases de données SQLite (données locales, non versionnées) --- # --- Bases de données SQLite (données locales, non versionnées) ---
*.db *.db
# Dossier de la base SQLite (bind mount Docker) — ne jamais versionner
/data/
*.db-shm *.db-shm
*.db-wal *.db-wal
+42 -9
View File
@@ -44,7 +44,7 @@
- **Service externe — adresses** : API BAN (Base Adresses Nationale) — `https://api-adresse.data.gouv.fr` — autocomplétion et géocodage des adresses françaises, sans clé d'authentification - **Service externe — adresses** : API BAN (Base Adresses Nationale) — `https://api-adresse.data.gouv.fr` — autocomplétion et géocodage des adresses françaises, sans clé d'authentification
- **Service externe — itinéraires** : OpenRouteService (ORS) — calcul de distance routière et géométrie d'itinéraire entre coordonnées géographiques connues - **Service externe — itinéraires** : OpenRouteService (ORS) — calcul de distance routière et géométrie d'itinéraire entre coordonnées géographiques connues
- **Génération PDF** : côté backend, bibliothèque QuestPDF - **Génération PDF** : côté backend, bibliothèque QuestPDF
- **Hébergement** : l'application est packagée et hébergée via **Docker Compose** — un conteneur `frontend` (Angular servi par nginx en interne) publié sur le port hôte **2202**, et un conteneur `backend` (ASP.NET Core API) non exposé à l'hôte. Le fichier SQLite est persisté sur un volume monté dans le conteneur backend. La terminaison **HTTPS est assurée par un reverse-proxy Caddy externe** (sur une autre machine) qui relaie vers `IP_hôte:2202` ; le backend ne fait donc pas de redirection HTTPS lui-même - **Hébergement** : l'application est packagée et hébergée via **Docker Compose** — un conteneur `frontend` (Angular servi par nginx en interne) publié sur le port hôte **2202**, et un conteneur `backend` (ASP.NET Core API) non exposé à l'hôte. Le fichier SQLite est persisté via un **bind mount** (`./data:/data`) dans le conteneur backend, ce qui rend `data/trajets.db` directement accessible sur l'hôte (inspection et sauvegarde sans entrer dans le conteneur). La terminaison **HTTPS est assurée par un reverse-proxy Caddy externe** (sur une autre machine) qui relaie vers `IP_hôte:2202` ; le backend ne fait donc pas de redirection HTTPS lui-même
--- ---
@@ -121,7 +121,7 @@ sequenceDiagram
> **Justification** : L'application est mono-utilisateur et n'a pas vocation à être multi-instances. SQLite élimine la complexité d'installation d'un serveur de base de données, offre des transactions ACID complètes via Entity Framework Core, et est parfaitement supporté par EF Core avec les migrations standard. Le fichier `trajets.db` est autoportant et facilement sauvegardable. > **Justification** : L'application est mono-utilisateur et n'a pas vocation à être multi-instances. SQLite élimine la complexité d'installation d'un serveur de base de données, offre des transactions ACID complètes via Entity Framework Core, et est parfaitement supporté par EF Core avec les migrations standard. Le fichier `trajets.db` est autoportant et facilement sauvegardable.
> >
> **Emplacement en conteneur** : dans le déploiement Docker Compose, le fichier de base de données est stocké sous le chemin du volume monté `/data/trajets.db` (voir la section « Déploiement (Docker Compose) »). Le chemin est fourni au backend via la chaîne de connexion / variable d'environnement (`ConnectionStrings__Default=Data Source=/data/trajets.db`), ce qui garantit la persistance des données indépendamment du cycle de vie du conteneur. > **Emplacement en conteneur** : dans le déploiement Docker Compose, le fichier de base de données est stocké sous `/data/trajets.db`, monté via un **bind mount** depuis `./data` de l'hôte (voir la section « Déploiement (Docker Compose) » et « Accès à la base de données »). Le chemin est fourni au backend via la chaîne de connexion / variable d'environnement (`ConnectionStrings__Default=Data Source=/data/trajets.db`), ce qui garantit la persistance des données indépendamment du cycle de vie du conteneur.
### Décision d'architecture : BAN pour le géocodage, ORS pour l'itinéraire ### Décision d'architecture : BAN pour le géocodage, ORS pour l'itinéraire
@@ -1554,7 +1554,7 @@ La génération PDF est enveloppée dans un `CancellationToken` avec délai de 1
La solution est hébergée via **Docker Compose** avec deux services : La solution est hébergée via **Docker Compose** avec deux services :
- **`backend`** : API ASP.NET Core (.NET 8), exposant le port `8080` en interne. Le fichier SQLite est stocké sous le chemin du volume monté `/data/trajets.db`. - **`backend`** : API ASP.NET Core (.NET 8), exposant le port `8080` en interne. Le fichier SQLite est stocké sous `/data/trajets.db`, monté depuis `./data` de l'hôte via un bind mount (accès direct au fichier).
- **`frontend`** : application Angular compilée et servie par **nginx**. Nginx sert la SPA (fallback vers `index.html` pour le routage côté client) et agit comme reverse-proxy en relayant les requêtes `/api` vers le service `backend`. C'est le seul service exposé à l'hôte (port **`2202`** de l'hôte → nginx). La terminaison HTTPS est assurée par un reverse-proxy **Caddy externe**. - **`frontend`** : application Angular compilée et servie par **nginx**. Nginx sert la SPA (fallback vers `index.html` pour le routage côté client) et agit comme reverse-proxy en relayant les requêtes `/api` vers le service `backend`. C'est le seul service exposé à l'hôte (port **`2202`** de l'hôte → nginx). La terminaison HTTPS est assurée par un reverse-proxy **Caddy externe**.
```mermaid ```mermaid
@@ -1564,7 +1564,7 @@ graph LR
NGINX[frontend<br/>nginx + SPA Angular] NGINX[frontend<br/>nginx + SPA Angular]
API[backend<br/>ASP.NET Core :8080] API[backend<br/>ASP.NET Core :8080]
NGINX -- "proxy /api → backend:8080" --> API NGINX -- "proxy /api → backend:8080" --> API
API -- "/data/trajets.db" --> VOL[(Volume nommé<br/>trajets-data)] API -- "/data/trajets.db" --> VOL[(Bind mount<br/>./data sur l'hôte)]
end end
``` ```
@@ -1664,7 +1664,8 @@ services:
# URL publique du frontend (liens de réinitialisation de mot de passe), ex. https://mon-domaine # URL publique du frontend (liens de réinitialisation de mot de passe), ex. https://mon-domaine
- Auth__FrontendBaseUrl=${AUTH_FRONTEND_BASE_URL} - Auth__FrontendBaseUrl=${AUTH_FRONTEND_BASE_URL}
volumes: volumes:
- trajets-data:/data # Bind mount : base accessible sur l'hôte dans ./data/trajets.db
- ./data:/data
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"] test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
@@ -1683,9 +1684,6 @@ services:
backend: backend:
condition: service_healthy condition: service_healthy
restart: unless-stopped restart: unless-stopped
volumes:
trajets-data: # persistance SQLite au-delà du cycle de vie des conteneurs
``` ```
> **Endpoint de santé** : le backend expose `GET /health` (via `AddHealthChecks()` / `MapHealthChecks("/health")`) utilisé par le `healthcheck` Compose et la condition `depends_on: service_healthy`. > **Endpoint de santé** : le backend expose `GET /health` (via `AddHealthChecks()` / `MapHealthChecks("/health")`) utilisé par le `healthcheck` Compose et la condition `depends_on: service_healthy`.
@@ -1697,7 +1695,7 @@ volumes:
- L'**URL publique du frontend** (`AUTH_FRONTEND_BASE_URL`, ex. `https://mon-domaine`) sert à construire les liens de réinitialisation de mot de passe envoyés par e-mail — et non le port interne. - L'**URL publique du frontend** (`AUTH_FRONTEND_BASE_URL`, ex. `https://mon-domaine`) sert à construire les liens de réinitialisation de mot de passe envoyés par e-mail — et non le port interne.
- La **configuration e-mail** (SMTP via MailKit) est optionnelle : si elle est absente, le lien de réinitialisation est journalisé en repli (fallback) au lieu d'être envoyé. - La **configuration e-mail** (SMTP via MailKit) est optionnelle : si elle est absente, le lien de réinitialisation est journalisé en repli (fallback) au lieu d'être envoyé.
- L'**API BAN** ne nécessite aucune clé — aucune configuration de secret n'est requise pour le géocodage. - L'**API BAN** ne nécessite aucune clé — aucune configuration de secret n'est requise pour le géocodage.
- Le **volume nommé `trajets-data`** (monté sur `/data`) garantit que le fichier `trajets.db` persiste à travers les redémarrages, recréations et mises à jour des conteneurs. - Le **bind mount `./data:/data`** garantit que le fichier `trajets.db` persiste à travers les redémarrages, recréations et mises à jour des conteneurs, tout en le rendant directement accessible sur l'hôte.
### Migrations au démarrage ### Migrations au démarrage
@@ -1705,6 +1703,41 @@ Les **migrations EF Core sont appliquées automatiquement au démarrage du backe
> **Documents de spécification embarqués** : le `Dockerfile` backend a pour contexte la racine du dépôt et copie `.kiro/specs/schotziad-km-tracker/{requirements,design,tasks}.md` dans `./specs/` de l'image. Le `SpecDocumentService` lit en priorité ce dossier embarqué (`ContentRootPath/specs`), avec repli sur `.kiro/specs/...` en développement. L'application est ainsi autonome, sans montage du dépôt à l'exécution. > **Documents de spécification embarqués** : le `Dockerfile` backend a pour contexte la racine du dépôt et copie `.kiro/specs/schotziad-km-tracker/{requirements,design,tasks}.md` dans `./specs/` de l'image. Le `SpecDocumentService` lit en priorité ce dossier embarqué (`ContentRootPath/specs`), avec repli sur `.kiro/specs/...` en développement. L'application est ainsi autonome, sans montage du dépôt à l'exécution.
### Accès à la base de données
La base SQLite est exposée à l'hôte via un **bind mount** (`./data:/data` dans `docker-compose.yml`) : le fichier `data/trajets.db` est directement lisible et sauvegardable **sans entrer dans le conteneur**, à côté du `docker-compose.yml`.
**Inspection / lecture** : ouvrir `data/trajets.db` avec un outil SQLite (DB Browser for SQLite, `sqlite3`, extension VS Code…). La lecture pendant que l'application tourne est sûre.
**Sauvegarde à froid (recommandée)** : SQLite tolère mal l'écriture concurrente. Pour un backup cohérent, arrêter le backend le temps de la copie, ou utiliser la commande de sauvegarde en ligne intégrée :
```bash
# Option 1 — snapshot à froid (cohérence garantie)
docker compose stop backend
cp data/trajets.db "backup-$(date +%F).db"
docker compose start backend
# Option 2 — sauvegarde en ligne SQLite (sans arrêt), si sqlite3 est installé sur l'hôte
sqlite3 data/trajets.db ".backup 'backup-$(date +%F).db'"
```
> **Concurrence** : éviter d'écrire dans `data/trajets.db` depuis l'extérieur pendant que l'application tourne (risque de corruption). La lecture reste sûre.
**Migration depuis l'ancien volume nommé** : les déploiements antérieurs utilisaient un volume nommé Docker (`trajets-data`). Pour récupérer ces données dans le nouveau bind mount `./data` avant de basculer :
```bash
# Extraire la base de l'ancien volume vers ./data (backend encore basé sur l'ancienne image)
mkdir -p data
docker compose cp backend:/data/trajets.db ./data/trajets.db # si le conteneur tourne encore
# — ou, si le service est déjà recréé, copier depuis le volume nommé :
docker run --rm -v trajets-data:/from -v "$PWD/data":/to alpine \
sh -c "cp -a /from/. /to/"
# Puis recréer avec le bind mount :
docker compose up -d --build
```
Après migration et vérification, l'ancien volume nommé peut être supprimé : `docker volume rm trajets-data`.
> **Migrations notables** : `VehiculeDenomination` (dénomination du véhicule), `TrajetGeometrieJson` (géométrie d'itinéraire portée par le trajet), `TrancheBaremeKmMaxNullable` / bornes exclusives / coefficient numérique (barème multi-tranches à dernière tranche illimitée), `AjoutAuthentification` (comptes + jetons de réinitialisation), `TrajetAllerRetour` (indicateur aller/retour). > **Migrations notables** : `VehiculeDenomination` (dénomination du véhicule), `TrajetGeometrieJson` (géométrie d'itinéraire portée par le trajet), `TrancheBaremeKmMaxNullable` / bornes exclusives / coefficient numérique (barème multi-tranches à dernière tranche illimitée), `AjoutAuthentification` (comptes + jetons de réinitialisation), `TrajetAllerRetour` (indicateur aller/retour).
## Error Handling ## Error Handling
+4 -4
View File
@@ -22,7 +22,10 @@ services:
- Smtp__Password=${SMTP_PASSWORD:-} - Smtp__Password=${SMTP_PASSWORD:-}
- Smtp__From=${SMTP_FROM:-} - Smtp__From=${SMTP_FROM:-}
volumes: volumes:
- trajets-data:/data # Bind mount : la base SQLite est directement accessible sur l'hôte dans ./data/trajets.db,
# sans entrer dans le conteneur (inspection, sauvegarde). Le dossier ./data est créé au
# premier démarrage s'il n'existe pas.
- ./data:/data
restart: unless-stopped restart: unless-stopped
# Healthcheck sur GET /health. curl est installé dans l'image backend (voir Dockerfile). # Healthcheck sur GET /health. curl est installé dans l'image backend (voir Dockerfile).
healthcheck: healthcheck:
@@ -42,6 +45,3 @@ services:
backend: backend:
condition: service_healthy condition: service_healthy
restart: unless-stopped restart: unless-stopped
volumes:
trajets-data: # persistance SQLite au-delà du cycle de vie des conteneurs
-92
View File
@@ -1,92 +0,0 @@
#!/usr/bin/env bash
# Lancement de SchotzIAD KM Tracker SANS docker compose (topologie équivalente).
# Utile quand docker compose est indisponible/cassé.
#
# Usage :
# ./run-docker.sh up # build + démarre backend et frontend
# ./run-docker.sh down # arrête et supprime les conteneurs (garde le volume/les données)
# ./run-docker.sh destroy # arrête tout ET supprime le volume SQLite (perte des données)
# ./run-docker.sh logs # affiche les logs des deux conteneurs
#
# Selon votre config Docker, préfixez par sudo : sudo ./run-docker.sh up
set -euo pipefail
# Détecte s'il faut sudo (utilisateur hors du groupe docker).
DOCKER="docker"
if ! docker info >/dev/null 2>&1; then
if sudo -n docker info >/dev/null 2>&1 || sudo docker info >/dev/null 2>&1; then
DOCKER="sudo docker"
echo "[info] Docker requiert sudo — utilisation de 'sudo docker'."
else
echo "[erreur] Démon Docker inaccessible (ni direct, ni via sudo)." >&2
exit 1
fi
fi
ROOT="$(cd "$(dirname "$0")" && pwd)"
NET="schotziad-net"
VOL="trajets-data"
ORS_KEY="${ORS_API_KEY:-}"
# Charge ORS_API_KEY depuis .env s'il existe et n'est pas déjà défini.
if [[ -z "$ORS_KEY" && -f "$ROOT/.env" ]]; then
ORS_KEY="$(grep -E '^ORS_API_KEY=' "$ROOT/.env" | head -n1 | cut -d= -f2- || true)"
fi
up() {
echo "[1/5] Réseau et volume…"
$DOCKER network inspect "$NET" >/dev/null 2>&1 || $DOCKER network create "$NET"
$DOCKER volume inspect "$VOL" >/dev/null 2>&1 || $DOCKER volume create "$VOL"
echo "[2/5] Build backend…"
$DOCKER build -t schotziad-backend "$ROOT/backend"
echo "[3/5] Build frontend…"
$DOCKER build -t schotziad-frontend "$ROOT/frontend"
echo "[4/5] Démarrage backend (nom d'hôte réseau: backend)…"
$DOCKER rm -f schotziad-backend >/dev/null 2>&1 || true
$DOCKER run -d --name schotziad-backend --network "$NET" --network-alias backend \
-e "ASPNETCORE_URLS=http://+:8080" \
-e "ConnectionStrings__Default=Data Source=/data/trajets.db" \
-e "ORS__ApiKey=${ORS_KEY}" \
-v "$VOL:/data" \
--restart unless-stopped \
schotziad-backend
echo "[5/5] Démarrage frontend (http://localhost:8080)…"
$DOCKER rm -f schotziad-frontend >/dev/null 2>&1 || true
$DOCKER run -d --name schotziad-frontend --network "$NET" \
-p 8080:80 \
--restart unless-stopped \
schotziad-frontend
echo
echo "Prêt. Ouvrez http://localhost:8080"
echo "Logs : ./run-docker.sh logs | Arrêt : ./run-docker.sh down"
}
down() {
$DOCKER rm -f schotziad-frontend schotziad-backend >/dev/null 2>&1 || true
echo "Conteneurs arrêtés (le volume '$VOL' et les données sont conservés)."
}
destroy() {
down
$DOCKER volume rm "$VOL" >/dev/null 2>&1 || true
$DOCKER network rm "$NET" >/dev/null 2>&1 || true
echo "Volume, réseau et données supprimés."
}
logs() {
echo "=== backend ==="; $DOCKER logs --tail 40 schotziad-backend 2>&1 || true
echo "=== frontend ==="; $DOCKER logs --tail 20 schotziad-frontend 2>&1 || true
}
case "${1:-up}" in
up) up ;;
down) down ;;
destroy) destroy ;;
logs) logs ;;
*) echo "Usage: $0 {up|down|destroy|logs}" >&2; exit 2 ;;
esac