From 84998608d5b37a67eb3c2db16a3f2f77a55d1f3d Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Mon, 2 Nov 2026 20:37:00 +0100 Subject: [PATCH 1/2] =?UTF-8?q?D=C3=A9ploiement=20CapRover=20:=20site=20st?= =?UTF-8?q?atique=20construit=20sur=20le=20PC,=20servi=20par=20Caddy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CREDITS.md | 5 +++ Makefile | 31 +++++++++++++- deploy/Caddyfile | 39 ++++++++++++++++++ deploy/Dockerfile | 9 +++++ deploy/captain-definition | 1 + docs/deploiement.md | 85 +++++++++++++++++++++++++++++++++++++++ scripts/deploy.sh | 46 +++++++++++++++++++++ tests/test_site.sh | 54 +++++++++++++++++++++++++ 8 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 deploy/Caddyfile create mode 100644 deploy/Dockerfile create mode 100644 deploy/captain-definition create mode 100644 docs/deploiement.md create mode 100755 scripts/deploy.sh create mode 100755 tests/test_site.sh diff --git a/CREDITS.md b/CREDITS.md index f7e62ec..a7df47d 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -50,6 +50,11 @@ Les contenus pédagogiques (leçons, textes) sont sous licence CC BY-SA 4.0. fournit aussi **Node.js** (MIT), qui rejoue la suite de contrat sur le moteur WebAssembly et lance `make bench`. +## Serveur de production + +- **Caddy** (Apache-2.0), image `caddy:2.8.4-alpine`, serveur statique du + site publié sur CapRover (`deploy/`). + ## Remerciements À l'Atelier du Huit, café associatif de Cugnaux, et à Philippe, qui y transmet le jeu. diff --git a/Makefile b/Makefile index 861c59e..ea52efa 100644 --- a/Makefile +++ b/Makefile @@ -22,15 +22,17 @@ BUILD_C_IMAGE := ustagammon/build-c:2 WASM_IMAGE := emscripten/emsdk:3.1.64 # Port de la page de test servie sur le réseau local (make deploy-web). WEB_PORT := 8080 +# Serveur statique de production (deploy/Dockerfile) : même version ici. +CADDY_IMAGE := caddy:2.8.4-alpine -export SHELLCHECK_IMAGE GHERKIN_IMAGE BUILD_C_IMAGE WASM_IMAGE +export SHELLCHECK_IMAGE GHERKIN_IMAGE BUILD_C_IMAGE WASM_IMAGE CADDY_IMAGE # Lance une commande dans la chaîne C, sous l'utilisateur courant pour que # build/ lui appartienne. DOCKER_C := docker run --rm --user "$$(id -u):$$(id -g)" -v "$(CURDIR):/src" -w /src $(BUILD_C_IMAGE) DOCKER_WASM := docker run --rm --user "$$(id -u):$$(id -g)" -e HOME=/tmp -v "$(CURDIR):/src" -w /src $(WASM_IMAGE) -.PHONY: help setup build engine engine-wasm bench test lint deploy deploy-web clean reference-image reference +.PHONY: help setup build engine engine-wasm bench test lint deploy deploy-web site package deploy-prod clean reference-image reference help: ## Liste les commandes disponibles @echo "Ustagammon : commandes disponibles" @@ -48,6 +50,8 @@ setup: ## Installe la chaîne d'outils (images Docker) ; sans effet si déjà fa || docker build -t $(BUILD_C_IMAGE) docker/build-c @docker image inspect $(WASM_IMAGE) >/dev/null 2>&1 \ || docker pull $(WASM_IMAGE) + @docker image inspect $(CADDY_IMAGE) >/dev/null 2>&1 \ + || docker pull $(CADDY_IMAGE) @echo "Chaîne d'outils prête." build: engine ## Compile le moteur (si nécessaire) puis l'application @@ -92,6 +96,29 @@ deploy-web: engine-wasm ## Sert la page de test WebAssembly sur le réseau local sh -c 'cp -r /web /tmp/site && cp -r /wasm /tmp/site/wasm \ && exec python3 -m http.server 8080 --directory /tmp/site' +# Le site tel qu'il sera publié : web/ à la racine, le moteur sous wasm/, et +# version.json (commit déployé). Reconstruit entièrement à chaque appel. +site: engine-wasm ## Assemble le site publié dans build/site/ + rm -rf $(BUILD_DIR)/site + mkdir -p $(BUILD_DIR)/site/wasm + cp -r web/. $(BUILD_DIR)/site/ + cp $(BUILD_DIR)/wasm/engine.mjs $(BUILD_DIR)/wasm/engine.wasm $(BUILD_DIR)/wasm/engine.data \ + $(BUILD_DIR)/wasm/positions.json $(BUILD_DIR)/site/wasm/ + printf '{"commit": "%s", "dirty": %s}\n' "$$(git rev-parse HEAD)" \ + "$$(git diff --quiet HEAD && echo false || echo true)" > $(BUILD_DIR)/site/version.json + +# L'archive envoyée à CapRover : captain-definition, Dockerfile, Caddyfile et +# le site. CapRover construit l'image (une simple copie) sur le serveur. +package: site ## Prépare l'archive de déploiement build/ustagammon.tar + rm -rf $(BUILD_DIR)/deploy + cp -r deploy $(BUILD_DIR)/deploy + cp -r $(BUILD_DIR)/site $(BUILD_DIR)/deploy/site + tar -cf $(BUILD_DIR)/ustagammon.tar -C $(BUILD_DIR)/deploy . + @echo "Archive prête : $(BUILD_DIR)/ustagammon.tar" + +deploy-prod: ## Publie main sur CapRover (voir docs/deploiement.md) + @scripts/deploy.sh + clean: ## Supprime les artefacts de build (garde les images Docker) rm -rf $(BUILD_DIR) diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..4d4e338 --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,39 @@ +# Serveur statique d'Ustagammon. CapRover fournit le HTTPS devant ; ici on +# écoute en HTTP sur le port 80 du conteneur. +{ + admin off + auto_https off +} + +:80 { + root * /srv + # Types compressés : ceux de Caddy par défaut, plus les données du moteur + # (application/octet-stream, 8,7 Mo, 7,2 Mo compressés). + encode { + zstd + gzip + match { + header Content-Type text/* + header Content-Type application/json* + header Content-Type application/javascript* + header Content-Type application/wasm* + header Content-Type application/octet-stream* + header Content-Type application/manifest+json* + header Content-Type image/svg+xml* + } + } + + # Les noms de fichiers ne changent pas d'une version à l'autre : le + # navigateur revalide chaque fichier (ETag) au lieu de garder une copie + # périmée. + header Cache-Control "no-cache" + header X-Content-Type-Options "nosniff" + header Referrer-Policy "no-referrer" + + # Données du moteur (Emscripten) : extension inconnue de Caddy, qui + # devinerait text/xml d'après le début du fichier. + @engine_data path *.data + header @engine_data Content-Type "application/octet-stream" + + file_server +} diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..8641172 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,9 @@ +# Image de production d'Ustagammon : un serveur statique (Caddy) et le site +# déjà construit et testé sur le PC (make site). Rien n'est compilé sur le +# serveur : voir docs/deploiement.md. +FROM caddy:2.8.4-alpine + +COPY Caddyfile /etc/caddy/Caddyfile +COPY site/ /srv/ + +EXPOSE 80 diff --git a/deploy/captain-definition b/deploy/captain-definition new file mode 100644 index 0000000..e0d2188 --- /dev/null +++ b/deploy/captain-definition @@ -0,0 +1 @@ +{"schemaVersion": 2, "dockerfilePath": "./Dockerfile"} diff --git a/docs/deploiement.md b/docs/deploiement.md new file mode 100644 index 0000000..76a7a7b --- /dev/null +++ b/docs/deploiement.md @@ -0,0 +1,85 @@ +# Déploiement + +Ustagammon est un site statique (PWA, ADR 0009) : des fichiers HTML, +JavaScript, le moteur en WebAssembly et ses données. Il est publié sur +CapRover, machine `sans-pub` (https://captain.cloud.sans.pub), app +`ustagammon`. + +**Le site est construit et testé sur le PC, pas sur le serveur.** La +construction est lourde (Emscripten, génération des données du moteur) et déjà +outillée en local. `make deploy-prod` envoie à CapRover une archive qui +contient le site prêt, un `Dockerfile` et un `Caddyfile` (`deploy/`). Le serveur +ne fait que copier les fichiers dans une image Caddy (`caddy:2.8.4-alpine`), +qui les sert en HTTP derrière le HTTPS de CapRover. + +## Prérequis + +- La chaîne d'outils du projet : `make setup`. +- **CLI CapRover**, installée avec Node par `fnm` (le script la trouve seul) : + ```bash + eval "$(fnm env)"; fnm use lts-latest + ``` +- **Session CapRover valide.** Le jeton expire ; pour vérifier : + ```bash + caprover api -n sans-pub -t /user/system/info -m GET -d '{}' -o false + ``` + En cas de `not valid anymore` : `caprover login` dans un vrai terminal. +- **L'app `ustagammon` existe dans CapRover** (une fois) : *Apps*, créer + `ustagammon`, sans volume (rien n'est stocké côté serveur), port du conteneur + 80. Activer le HTTPS et, si besoin, un domaine. + +## Publier + +Depuis `main`, à jour avec `origin/main` (après fusion de la MR) : + +``` +make deploy-prod +``` + +Le script refuse de publier si l'arbre n'est pas propre, si l'on n'est pas sur +`main`, si `main` diffère d'`origin/main`, ou si la session CapRover a expiré. +Il lance `make lint` et `make test`, assemble le site (`make package`), puis +`caprover deploy -t build/ustagammon.tar`. + +Pour une autre machine ou une autre app : `CAPROVER_MACHINE=… CAPROVER_APP=… +make deploy-prod`. + +## Ce qui est publié + +- `make site` assemble `build/site/` : le contenu de `web/` à la racine, le + moteur (`engine.mjs`, `engine.wasm`, `engine.data`, `positions.json`) sous + `wasm/`, et `version.json` avec le commit publié. +- `make package` ajoute `deploy/` et produit `build/ustagammon.tar` (environ + 9 Mo). +- `tests/test_site.sh` (dans `make test`) construit l'image de production en + local, la lance et vérifie ce que reçoit un navigateur : pages, types MIME + (`application/wasm` pour le moteur), compression gzip des données, en-têtes, + et commit de `version.json`. + +Réglages du serveur (`deploy/Caddyfile`) : compression zstd ou gzip, y compris +pour les données du moteur ; `Cache-Control: no-cache` (les noms de fichiers ne +changent pas d'une version à l'autre, le navigateur revalide chaque fichier) ; +`X-Content-Type-Options: nosniff` ; `Referrer-Policy: no-referrer`. + +## Vérifier + +```bash +curl -s https:///version.json # commit publié +curl -sI https:///wasm/engine.wasm # application/wasm +ssh sans.pub 'sudo docker service ps srv-captain--ustagammon --format "{{.Image}} {{.CurrentState}}" | head -2' +``` + +Puis ouvrir le site sur le téléphone : la page de test doit afficher « Moteur +prêt ». + +## Retour arrière + +Dans CapRover, onglet *Deployment* de l'app `ustagammon` : redéployer une image +précédente (`img-captain-ustagammon:N`). Rien n'est stocké côté serveur : aucun +risque pour des données. + +## Incidents connus + +Voir `la-main-libre` (`docs/technique/deploiement.md`) pour le registre Docker +de CapRover dont le certificat expire : le build réussit, l'envoi échoue, la +production n'est pas touchée ; redémarrer `captain-registry` puis relancer. diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..d8194b1 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Publie la branche main sur CapRover (voir docs/deploiement.md). +# +# Le site est construit et testé sur le PC, puis envoyé sous forme d'archive : +# le serveur ne fait que copier les fichiers dans une image Caddy. +# +# Prérequis : CLI caprover (via fnm) connecté à la machine CapRover +# (caprover login). + +set -euo pipefail + +CAPROVER_MACHINE="${CAPROVER_MACHINE:-sans-pub}" +CAPROVER_APP="${CAPROVER_APP:-ustagammon}" + +die() { + echo "Erreur : $1" >&2 + exit 1 +} + +cd "$(git rev-parse --show-toplevel)" + +# La CLI caprover est installée avec Node par fnm. +if ! command -v caprover >/dev/null && command -v fnm >/dev/null; then + eval "$(fnm env --shell bash)" + fnm use --silent-if-unchanged lts-latest >/dev/null || true +fi +command -v caprover >/dev/null || die "CLI caprover introuvable (voir docs/deploiement.md)" + +# On ne publie que main, propre et identique à origin/main. +[ "$(git branch --show-current)" = "main" ] || die "lancer le déploiement depuis la branche main" +git diff --quiet HEAD || die "des modifications ne sont pas commitées" +git fetch -q origin main +[ "$(git rev-parse main)" = "$(git rev-parse origin/main)" ] \ + || die "main diffère d'origin/main : git pull (ou fusionner la MR) d'abord" + +# Le jeton CapRover expire ; caprover ls ne le vérifie pas. +caprover api -n "$CAPROVER_MACHINE" -t /user/system/info -m GET -d '{}' -o false >/dev/null 2>&1 \ + || die "session CapRover expirée : lancer caprover login dans un terminal" + +echo "Vérification avant publication : make lint et make test" +make lint +make test + +make package +echo "Publication de main ($(git rev-parse --short main)) sur l'app $CAPROVER_APP" +caprover deploy -n "$CAPROVER_MACHINE" -a "$CAPROVER_APP" -t build/ustagammon.tar diff --git a/tests/test_site.sh b/tests/test_site.sh new file mode 100755 index 0000000..42565cb --- /dev/null +++ b/tests/test_site.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Vérifie l'image de production (deploy/) avec le site assemblé (make package) : +# construit l'image en local, la lance, et contrôle ce que reçoit un +# navigateur (pages, types MIME du moteur, compression, version). + +set -euo pipefail +cd "$(dirname "$0")/.." + +unset MAKEFLAGS MFLAGS MAKELEVEL +make -s package >/dev/null + +image="ustagammon/site-test:local" +docker build -q -t "$image" build/deploy >/dev/null +container="$(docker run -d --rm -p 127.0.0.1::80 "$image")" +trap 'docker stop "$container" >/dev/null' EXIT +port="$(docker port "$container" 80/tcp | head -n1 | sed 's/.*://')" +base="http://127.0.0.1:$port" + +# Caddy démarre en une fraction de seconde : on attend qu'il réponde. +for _ in $(seq 1 50); do + curl -sf -o /dev/null "$base/" && break + sleep 0.1 +done + +errors=0 +report() { + echo "$1" + errors=$((errors + 1)) +} + +# check +check() { + local headers + headers="$(curl -s -D - -o /dev/null "$base/$1")" + grep -qE "^HTTP/1.1 200" <<<"$headers" || report "/$1 : pas de réponse 200" + grep -qiE "^Content-Type: $2" <<<"$headers" || report "/$1 : type MIME attendu $2" +} + +check "" "text/html" +check "bench.html" "text/html" +check "engine-worker.mjs" "text/javascript" +check "wasm/engine.mjs" "text/javascript" +check "wasm/engine.wasm" "application/wasm" +check "wasm/engine.data" "application/octet-stream" +check "version.json" "application/json" + +curl -s -H "Accept-Encoding: gzip" -D - -o /dev/null "$base/wasm/engine.data" \ + | grep -qi "^Content-Encoding: gzip" || report "/wasm/engine.data : pas de compression gzip" +curl -s -D - -o /dev/null "$base/" | grep -qi "^Cache-Control: no-cache" \ + || report "/ : en-tête Cache-Control absent" +[ "$(curl -s "$base/version.json" | sed -n 's/.*"commit": "\([0-9a-f]*\)".*/\1/p')" = "$(git rev-parse HEAD)" ] \ + || report "version.json ne donne pas le commit courant" + +[ "$errors" -eq 0 ] -- 2.54.0 From c158d571119d8501c9f595b6615ffa617168980d Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Mon, 2 Nov 2026 21:04:37 +0100 Subject: [PATCH 2/2] =?UTF-8?q?D=C3=A9ploiement=20:=20domaine=20ustagammon?= =?UTF-8?q?.sans.pub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/deploiement.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/deploiement.md b/docs/deploiement.md index 76a7a7b..35bb54f 100644 --- a/docs/deploiement.md +++ b/docs/deploiement.md @@ -3,7 +3,7 @@ Ustagammon est un site statique (PWA, ADR 0009) : des fichiers HTML, JavaScript, le moteur en WebAssembly et ses données. Il est publié sur CapRover, machine `sans-pub` (https://captain.cloud.sans.pub), app -`ustagammon`. +`ustagammon`, sur https://ustagammon.sans.pub. **Le site est construit et testé sur le PC, pas sur le serveur.** La construction est lourde (Emscripten, génération des données du moteur) et déjà @@ -24,9 +24,9 @@ qui les sert en HTTP derrière le HTTPS de CapRover. caprover api -n sans-pub -t /user/system/info -m GET -d '{}' -o false ``` En cas de `not valid anymore` : `caprover login` dans un vrai terminal. -- **L'app `ustagammon` existe dans CapRover** (une fois) : *Apps*, créer - `ustagammon`, sans volume (rien n'est stocké côté serveur), port du conteneur - 80. Activer le HTTPS et, si besoin, un domaine. +- **L'app `ustagammon` existe dans CapRover** (créée le 2026-11-02) : sans + volume (rien n'est stocké côté serveur), port du conteneur 80, domaine + `ustagammon.sans.pub` avec HTTPS forcé. ## Publier @@ -64,8 +64,8 @@ changent pas d'une version à l'autre, le navigateur revalide chaque fichier) ; ## Vérifier ```bash -curl -s https:///version.json # commit publié -curl -sI https:///wasm/engine.wasm # application/wasm +curl -s https://ustagammon.sans.pub/version.json # commit publié +curl -sI https://ustagammon.sans.pub/wasm/engine.wasm # application/wasm ssh sans.pub 'sudo docker service ps srv-captain--ustagammon --format "{{.Image}} {{.CurrentState}}" | head -2' ``` -- 2.54.0