Servir et déployer
Le site est un dossier de fichiers statiques. Il peut être servi par n'importe quel serveur HTTP, mais deux configurations sont versionnées : le serveur de développement serve piloté par serve.json, et une image Docker nginx décrite par le Dockerfile et le dossier docker/.
Servir en local
Depuis le dossier du site :
cd frontend-office-tourisme
npm run devLe script dev de package.json lance npx --yes serve@14.2.7 . -l 4000. Il n'y a pas de npm ci à faire : npx télécharge la version exacte de serve au premier lancement, ce qui demande un accès à l'annuaire npm. Le site est alors disponible sur http://localhost:4000.
serve lit serve.json :
| Réglage | Valeur | Effet |
|---|---|---|
cleanUrls |
false |
Les URL gardent leur extension .html, comme en production |
trailingSlash |
false |
Pas de redirection vers une URL terminée par / |
directoryListing |
false |
Pas de liste des fichiers d'un dossier |
rewrites |
/ vers /index.html |
Page d'accueil |
En-têtes des .css, .js, .mjs |
Cache-Control: no-cache |
Chaque modification est visible au rechargement |
| En-têtes des images | Cache-Control: public, max-age=3600 |
Une heure de cache |
En-têtes des .html et de / |
no-cache, X-Content-Type-Options, Referrer-Policy, Content-Security-Policy, X-Frame-Options |
Même politique de sécurité de contenu qu'en production |
Servir le site avec la même politique de sécurité de contenu qu'en production permet de détecter en développement un style en ligne ou une ressource externe qui serait bloqué une fois déployé.
D'autres serveurs conviennent pour un essai rapide, sans les en-têtes : l'extension Live Server de VS Code, ou python -m http.server 4000. Ouvrir directement index.html depuis le disque ne fonctionne pas : le navigateur bloque le chargement des modules ES en file://.
Image Docker
FROM nginxinc/nginx-unprivileged:1.30-alpine
USER root
RUN apk upgrade --no-cache
USER 101
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY docker/security-headers.conf /etc/nginx/snippets/security-headers.conf
COPY --chown=nginx:nginx *.html /usr/share/nginx/html/
COPY --chown=nginx:nginx assets /usr/share/nginx/html/assets
COPY --chown=nginx:nginx .well-known /usr/share/nginx/html/.well-known
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1| Choix | Raison |
|---|---|
Image nginx-unprivileged |
nginx tourne sous l'utilisateur 101 et écoute le port 8080 : aucun privilège n'est nécessaire pour ouvrir un port inférieur à 1024 |
apk upgrade en root, puis retour à l'utilisateur 101 |
Applique les correctifs Alpine publiés après l'image de base, pour passer le contrôle Trivy de la CI |
| Copie sélective | Seuls les pages, assets/, .well-known/ et la configuration nginx entrent dans l'image |
HEALTHCHECK sur /health |
Docker et Compose savent si le serveur répond |
Le .dockerignore écarte README.md, package.json, serve.json, le Dockerfile et les journaux : ces fichiers ne sont pas servis en production.
Construire et lancer l'image seule, depuis la racine du dépôt :
docker build -t blocklucky-tourisme frontend-office-tourisme
docker run --rm -p 4000:8080 blocklucky-tourismeConfiguration nginx
docker/nginx.conf définit un seul serveur :
| Directive | Effet |
|---|---|
listen 8080, server_tokens off |
Port non privilégié, version de nginx masquée |
client_max_body_size 16k |
Corps de requête limité : le site ne reçoit jamais de données |
gzip on niveau 6, au-delà de 1024 octets |
Compression du CSS, du JavaScript, du SVG, du JSON et du texte |
if ($request_method !~ ^(GET|HEAD)$) { return 405; } |
Toute autre méthode HTTP est refusée |
location = /health |
Répond 200 ok en texte, sans journal d'accès |
location = /.well-known/security.txt |
Sert le fichier de contact sécurité, cache d'un jour |
location ~ /\.(?!well-known) |
Tout autre chemin commençant par un point renvoie 404 |
error_page 404 /404.html |
Page d'erreur du site |
Les durées de cache dépendent du type de fichier :
| Fichiers | Cache-Control |
|---|---|
.html |
no-cache : le navigateur revalide à chaque visite |
.css, .js, .mjs |
public, max-age=86400, stale-while-revalidate=604800 : un jour, puis une semaine de revalidation en arrière-plan |
Images et polices (jpg, jpeg, png, webp, avif, svg, ico, woff2) |
public, max-age=2592000 : 30 jours |
À savoir
Les noms des fichiers CSS et JavaScript ne contiennent pas d'empreinte. Après une mise à jour, un visiteur peut garder l'ancienne version d'un module jusqu'à un jour, tandis que les pages HTML, revalidées, sont déjà à jour. Pour une modification incompatible entre une page et un module, renommer le module ou ajouter un paramètre de version dans l'URL du script.
En-têtes de sécurité
Les en-têtes sont regroupés dans docker/security-headers.conf :
| En-tête | Valeur | Rôle |
|---|---|---|
Content-Security-Policy |
voir ci-dessous | Liste blanche des ressources |
Strict-Transport-Security |
max-age=63072000; includeSubDomains |
HTTPS obligatoire pendant deux ans, sous-domaines compris. Ignoré par le navigateur tant que le site est servi en HTTP, comme sur le port 4000 local |
X-Content-Type-Options |
nosniff |
Le navigateur respecte le type MIME annoncé |
X-Frame-Options |
DENY |
Le site ne peut pas être affiché dans un cadre |
Referrer-Policy |
strict-origin-when-cross-origin |
Seule l'origine est transmise aux autres sites |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=() |
Fonctions sensibles du navigateur désactivées |
Cross-Origin-Opener-Policy |
same-origin |
Isolement de la fenêtre vis-à-vis des pages ouvertes |
Cross-Origin-Resource-Policy |
same-origin |
Les ressources du site ne peuvent pas être chargées par un autre site |
Attention
Dans nginx, un bloc location qui déclare son propre add_header n'hérite plus d'aucun add_header du niveau server. C'est pourquoi chaque location qui ajoute un Cache-Control refait include /etc/nginx/snippets/security-headers.conf. Toute nouvelle location avec un add_header doit en faire autant, sinon ses réponses partent sans politique de sécurité.
Politique de sécurité de contenu
default-src 'self'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'La même valeur est écrite dans security-headers.conf et dans serve.json.
| Directive | Conséquence |
|---|---|
script-src 'self' |
Aucun script en ligne, aucun script tiers. Tous les comportements sont dans des modules de assets/js/ |
style-src 'self' |
Feuilles de style du site uniquement, pas de balise <style> |
style-src-attr 'unsafe-inline' |
Les attributs style="..." restent autorisés. Ils sont utilisés dans le HTML des pages (espacements ponctuels) et dans quelques gabarits JavaScript |
img-src 'self' data: |
Images du site et images en data: (icône SVG intégrée dans layout.css) |
font-src 'self' |
Polices auto-hébergées uniquement |
connect-src 'self' |
Aucune requête réseau vers un autre domaine, en particulier vers l'API BlockLucky |
object-src 'none', base-uri 'none', frame-ancestors 'none' |
Ni plugin, ni changement de l'URL de base, ni affichage en cadre |
form-action 'self' |
Un formulaire ne peut être envoyé qu'au site lui-même |
Le lien « Accéder à la loterie » n'est pas concerné : une navigation vers un autre site n'est pas une ressource chargée.
Dans Docker Compose
Le service tourisme de docker-compose.yml fait partie du profil app :
tourisme:
profiles: ["app"]
<<: *hardening
build:
context: ./frontend-office-tourisme
ports:
- "4000:8080"
restart: unless-stopped| Propriété | Effet |
|---|---|
profiles: ["app"] |
Démarré seulement avec docker compose --profile app up, avec le reste de l'application |
<<: *hardening |
Système de fichiers en lecture seule, toutes les capacités Linux retirées, no-new-privileges, /tmp en mémoire (64 Mo) pour les fichiers temporaires de nginx |
ports: "4000:8080" |
Le port 8080 du conteneur est publié sur le port 4000 de toutes les interfaces de l'hôte |
| aucune dépendance | Le service ne dépend ni de la base, ni de l'API |
Pour ne lancer que ce service :
docker compose --profile app up -d --build tourismeLe contexte de construction et la stack complète sont décrits dans Docker Compose et Images Docker.
Vérifications de la CI
Le job tourisme de .github/workflows/ci.yml s'exécute quand un fichier de frontend-office-tourisme/ ou le workflow change :
| Étape | Commande | Échoue si |
|---|---|---|
| Syntaxe des modules JavaScript | find assets/js \( -name '*.js' -o -name '*.mjs' \) -exec node --check {} \; |
Un fichier ne se compile pas |
| Ressources locales référencées par les pages HTML | node ../.github/scripts/check-static-links.mjs . |
Un src ou href d'une page HTML vise un fichier absent |
Le script check-static-links.mjs ignore les liens absolus (https:, mailto:, tel:), les ancres seules et les gabarits ${...}, puis retire paramètres et ancre avant de tester l'existence du fichier. Il affiche 14 pages vérifiées, aucune ressource locale manquante quand tout va bien.
Le job docker construit ensuite l'image, puis Trivy l'analyse et échoue sur toute vulnérabilité CRITICAL ou HIGH pour laquelle un correctif existe (--ignore-unfixed). Dependabot propose chaque semaine les nouvelles versions de l'image de base (écosystème docker, voir Dépendances).
Avant une mise en ligne
La cible prévue est tourisme.etherbay.tech, derrière le proxy Caddy de la machine de démonstration (voir Environnements et Déploiement sur etherbay.tech). Le conteneur sert du HTTP ; Caddy termine le HTTPS.
| Point | Action |
|---|---|
| Lien vers la loterie | Remplacer lotteryAppUrl: 'http://localhost:3000' dans assets/js/data/site.js par l'URL publique du frontend web, puis reconstruire l'image |
| Exposition du port | Le service publie 4000 sur toutes les interfaces. Derrière un proxy sur la même machine, limiter la publication à 127.0.0.1:4000:8080 pour que seul Caddy y accède |
| HSTS | L'en-tête porte includeSubDomains : il ne doit être envoyé que si tous les sous-domaines d'etherbay.tech sont servis en HTTPS |
| Dates des contenus | Vérifier que les événements de assets/js/data/events.js couvrent la période de démonstration (voir Contenus) |
security.txt |
Le champ Expires de .well-known/security.txt vaut 2027-11-10T00:00:00.000Z : le renouveler avant cette date |
| Mentions légales | Elles décrivent un site de démonstration : à réécrire si le site devenait réel |