Fusion de la MR !10 : déploiement CapRover
This commit was merged in pull request #10.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion": 2, "dockerfilePath": "./Dockerfile"}
|
||||
@@ -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`, 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à
|
||||
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** (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
|
||||
|
||||
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://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'
|
||||
```
|
||||
|
||||
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.
|
||||
Executable
+46
@@ -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
|
||||
Executable
+54
@@ -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 <chemin> <début du type MIME attendu>
|
||||
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 ]
|
||||
Reference in New Issue
Block a user