Checkpoint: Le stockage PDF devient un volume Docker externe explicitement nommé, avec nocopy. Le conteneur refuse de démarrer si /app/storage n’est pas un montage inscriptible, et son healthcheck contrôle aussi ce montage. Les tests, TypeScript et build ont été validés.
All checks were successful
Validation applicative / TypeScript, tests et build (push) Successful in 3m20s

This commit is contained in:
Manus
2026-09-03 09:20:33 +00:00
parent eb2c9cc6b6
commit a13fe40eb4
5 changed files with 61 additions and 9 deletions

View File

@@ -63,10 +63,11 @@ COPY --from=builder /app/dist ./dist
# Copy drizzle migrations
COPY drizzle/ ./drizzle/
COPY drizzle.config.ts ./
COPY scripts/ensure-storage-volume.sh ./scripts/ensure-storage-volume.sh
# Create uploads directory
RUN mkdir -p /app/uploads
# Create uploads directory and make the PDF volume guard executable.
RUN mkdir -p /app/uploads && chmod 755 /app/scripts/ensure-storage-volume.sh
EXPOSE 3000
CMD ["node", "dist/index.js"]
CMD ["/app/scripts/ensure-storage-volume.sh"]

View File

@@ -40,6 +40,8 @@ services:
JWT_SECRET: demat-facturation-jwt-secret-recette-2026
MISTRAL_API_KEY: ${MISTRAL_API_KEY:-}
UPLOAD_DIR: /app/uploads
# Chemin unique utilisé par le serveur et vérifié au démarrage.
STORAGE_BASE_PATH: /app/storage
# Manus OAuth (optionnel en recette)
VITE_APP_ID: ${VITE_APP_ID:-demat-facturation-recette}
OAUTH_SERVER_URL: ${OAUTH_SERVER_URL:-}
@@ -49,8 +51,12 @@ services:
volumes:
- demat_facturation_uploads:/app/uploads
# Les factures source et leurs métadonnées sont servies sous /storage.
# Ce volume doit rester monté à chaque reconstruction du conteneur.
- demat_facturation_storage:/app/storage
# Il est externe et n'est donc jamais recréé ni supprimé par Compose.
- type: volume
source: demat_facturation_storage
target: /app/storage
volume:
nocopy: true
networks:
- demat-facturation-internal
- web
@@ -61,7 +67,7 @@ services:
- "traefik.http.routers.demat-facturation.tls.certresolver=letsencrypt"
- "traefik.http.services.demat-facturation.loadbalancer.server.port=3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
test: ["CMD-SHELL", "grep -qs ' /app/storage ' /proc/mounts && test -w /app/storage && curl -fsS http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
@@ -73,7 +79,8 @@ volumes:
demat_facturation_uploads:
driver: local
demat_facturation_storage:
driver: local
external: true
name: ${STORAGE_VOLUME_NAME:-demat-facturation-dsi_demat_facturation_storage}
networks:
demat-facturation-internal:

View File

@@ -0,0 +1,24 @@
#!/bin/sh
# Refuse de lancer l'application si le répertoire PDF attendu n'est pas un
# volume Docker réellement monté. Cette barrière évite de démarrer avec un
# dossier de conteneur vide après une reconstruction.
set -eu
storage_path="${STORAGE_BASE_PATH:-/app/storage}"
if [ ! -d "$storage_path" ]; then
echo "[StorageGuard] Répertoire de stockage absent : $storage_path" >&2
exit 1
fi
if ! awk -v expected="$storage_path" '$2 == expected { mounted = 1 } END { exit mounted ? 0 : 1 }' /proc/mounts; then
echo "[StorageGuard] Volume persistant non monté sur $storage_path ; démarrage annulé." >&2
exit 1
fi
if [ ! -w "$storage_path" ]; then
echo "[StorageGuard] Volume persistant non inscriptible : $storage_path" >&2
exit 1
fi
exec node dist/index.js

View File

@@ -2,13 +2,28 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
describe("routage Traefik", () => {
describe("routage et persistance de déploiement", () => {
it("utilise le domaine fourni par lenvironnement avec la recette comme repli", () => {
const composeFile = readFileSync(join(process.cwd(), "docker-compose.yml"), "utf8");
expect(composeFile).toContain(
"Host(`${APP_DOMAIN:-demat-facturation.recette.santinova-soft.org}`)"
);
expect(composeFile).toContain("demat_facturation_storage:/app/storage");
expect(composeFile).toContain("source: demat_facturation_storage");
expect(composeFile).toContain("target: /app/storage");
});
it("refuse de recréer le volume PDF et contrôle son montage avant le démarrage", () => {
const composeFile = readFileSync(join(process.cwd(), "docker-compose.yml"), "utf8");
const dockerfile = readFileSync(join(process.cwd(), "Dockerfile"), "utf8");
const guardScript = readFileSync(join(process.cwd(), "scripts", "ensure-storage-volume.sh"), "utf8");
expect(composeFile).toContain("STORAGE_BASE_PATH: /app/storage");
expect(composeFile).toContain("external: true");
expect(composeFile).toContain("nocopy: true");
expect(composeFile).toContain("grep -qs ' /app/storage ' /proc/mounts");
expect(dockerfile).toContain('CMD ["/app/scripts/ensure-storage-volume.sh"]');
expect(guardScript).toContain("Volume persistant non monté");
expect(guardScript).toContain("/proc/mounts");
});
});

View File

@@ -900,3 +900,8 @@
- [x] Corriger le composant PDF sans toucher aux factures ni aux volumes persistants
- [x] Vérifier louverture dun PDF en production
- [x] Promouvoir ADM.pareige et Olivier Pareige au rôle administrateur afin dafficher les factures globales
## Prévention du démontage des PDF après reconstruction
- [x] Auditer les montages de stockage et la chaîne de reconstruction en recette et production
- [x] Ajouter un contrôle bloquant avant reconstruction et une vérification après démarrage du volume `/app/storage`
- [ ] Vérifier les garde-fous sur les deux environnements sans modifier les données