Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions .claude/deploiement.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,37 @@ Après chaque merge sur `main` :
bash bin/deploy-all.sh
```

Chaîne exécutée sur chaque serveur :
`git pull` → `composer install --no-dev --no-scripts` → `cache:clear` → `assets:install` → `importmap:install` → `migrations` → `asset-map:compile`

`--no-scripts` évite l'exécution des scripts post-install Symfony Flex (`auto-scripts` : `cache:clear`, `assets:install %PUBLIC_DIR%`, `importmap:install`) pendant `composer install` — ajouté suite à des `Killed` (OOM) répétés sur le mutualisé o2switch pendant cette phase (2026-07-22), alors que le `composer install` en lui-même n'était pas en cause (`Nothing to install, update or remove`, dépendances déjà à jour). Les 3 commandes sautées sont rappelées explicitement ensuite, une par une (moins gourmand qu'un `composer install` qui les enchaîne toutes en une seule fois) — ne pas en retirer une sans vérifier qu'elle est bien redondante ailleurs dans le script.

> **OOM résiduel, ponctuel, une seule instance** (vécu le 2026-07-23) — même
> après le split `--no-scripts`, `doctrine:migrations:migrate` ou
> `asset-map:compile` peuvent encore se faire tuer (`Killed`, exit 255) sur
> une instance isolée pendant `deploy-all.sh` sans que ça se reproduise sur
> les 6 autres — pas un bug de script, la charge mémoire mutualisée
> o2switch varie d'un run à l'autre. `deploy-all.sh` s'arrête (`✖ <instance>
> — échec`) mais **les autres instances déjà traitées ne sont pas
> affectées**. Ne pas relancer `deploy.sh`/`deploy-all.sh` en entier pour
> réparer une seule instance (risque de retomber sur le piège
> `PRENOM_PRESET` ci-dessus) : rejouer uniquement la commande tuée en SSH
> direct (cf. exemple dans la section `bin/deploy.sh`) — un simple retry
> suffit en général, sans changer aucun code.
Chaîne exécutée sur chaque serveur, **une connexion SSH par étape** (depuis le
2026-07-23, cf. ci-dessous) :
`git pull` → `composer install --no-dev --no-scripts` → `install-ffmpeg` → `cache:clear` → `assets:install` → `importmap:install` → `migrations` → `asset-map:compile` → `deploy-info`

`--no-scripts` évite l'exécution des scripts post-install Symfony Flex (`auto-scripts` : `cache:clear`, `assets:install %PUBLIC_DIR%`, `importmap:install`) pendant `composer install` — ajouté suite à des `Killed` (OOM) répétés sur le mutualisé o2switch pendant cette phase (2026-07-22), alors que le `composer install` en lui-même n'était pas en cause (`Nothing to install, update or remove`, dépendances déjà à jour). Les 3 commandes sautées sont rappelées explicitement ensuite, une par une — ne pas en retirer une sans vérifier qu'elle est bien redondante ailleurs dans le script.

> **Cause racine identifiée et corrigée (2026-07-23)** — les `Killed` (OOM)
> observés à plusieurs reprises sur des instances isolées (une seule sur les
> 7, jamais la même, commande tuée différente à chaque fois : migrations,
> `cache:clear`…) ne viennent **pas** d'un manque de RAM sur la machine (`free
> -h` montre 15 Gio libres sur 125 Gio total) mais d'un **quota mémoire par
> compte cPanel** — le compte tourne dans un LVE CloudLinux
> (`/proc/self/cgroup` → `lve508`), un cgroup isolé de la RAM système totale.
> `deploy-all.sh` enchaînait toutes les commandes `bin/console` dans **un seul
> process SSH** ; l'empreinte mémoire (opcache, buffers) s'accumulait d'une
> commande à l'autre dans ce process jusqu'à dépasser le quota du LVE sur une
> étape qui, seule, tient largement dans ce même quota.
>
> **Correctif** : chaque étape (`composer install`, `cache:clear`,
> `assets:install`, `importmap:install`, `migrations`, `asset-map:compile`…)
> ouvre maintenant sa **propre connexion SSH** (fonction `run_step` dans
> `bin/deploy-all.sh`) — chaque étape démarre donc dans un process shell
> neuf côté serveur, sans hériter de l'empreinte mémoire des étapes
> précédentes. Validé le 2026-07-23 : 7/7 instances déployées sans échec,
> y compris les deux qui avaient été tuées par OOM lors des deux
> déploiements précédents avec l'ancienne version (une seule connexion SSH).
>
> Si un `✖ <instance> — échec à l'étape « <nom> »` apparaît malgré tout,
> rejouer uniquement cette étape en SSH direct (cf. exemple dans la section
> `bin/deploy.sh`) plutôt que de relancer tout `deploy-all.sh`/`deploy.sh`
> (risque de retomber sur le piège `PRENOM_PRESET` ci-dessus).

### Premier déploiement de toutes les instances

Expand Down
90 changes: 53 additions & 37 deletions bin/deploy-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,25 @@ REMOTE_HOME=$(ssh ${SSH_KEY_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" 'ec
declare -a RESULTS_OK=()
declare -a RESULTS_FAIL=()

# ── Exécution d'une étape distante dans son propre process SSH ───────────────
# Chaque étape ouvre une connexion SSH neuve (donc un process shell neuf côté
# serveur) au lieu d'enchaîner toutes les commandes dans un seul process —
# le compte tourne dans un LVE CloudLinux (quota mémoire par compte, isolé de
# la RAM totale de la machine) et un process qui accumule cache/opcache sur
# plusieurs commandes bin/console à la suite peut dépasser ce quota, même s'il
# reste beaucoup de RAM libre au niveau système (vécu 2026-07-23 : migrations
# puis cache:clear tués (Killed) sur des instances différentes lors de deux
# déploiements successifs).
run_step() {
local label="$1"
local remote_cmd="$2"
if ! ssh ${SSH_KEY_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" "set -e; cd ${DEPLOY_PATH}; ${remote_cmd}"; then
error "${SUBDOMAIN} — échec à l'étape « ${label} »"
return 1
fi
return 0
}

# ── Boucle sur les cibles ─────────────────────────────────────────────────────
for PRENOM in "${TARGETS[@]}"; do
SUBDOMAIN="${PRENOM}.lenouvel.me"
Expand Down Expand Up @@ -133,36 +152,38 @@ for PRENOM in "${TARGETS[@]}"; do
JWT_PASSPHRASE=$(php -r "echo bin2hex(random_bytes(24));")
DATABASE_URL="mysql://${DB_USER}:${DB_PASSWORD}@127.0.0.1:3306/${DB_NAME}?serverVersion=mariadb-10.6.0&charset=utf8mb4"

info "Clonage et setup initial…"
if ssh ${SSH_KEY_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" "
set -e
mkdir -p ${DEPLOY_PATH}
cd ${DEPLOY_PATH}
git clone ${GIT_REPO} .
mkdir -p var/log
cat > .env.local <<'ENVEOF'
APP_ENV=prod
info "Clonage et setup initial (étapes séparées)…"
DEPLOY_INFO_LINE="<!-- Deployed: $(date '+%Y-%m-%d %H:%M:%S') -->"
ENV_LOCAL_CONTENT="APP_ENV=prod
APP_SECRET=${APP_SECRET}
DATABASE_URL=${DATABASE_URL}
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=${JWT_PASSPHRASE}
MAILER_DSN=${MAILER_DSN_PRESET:-null://null}
MAILER_DSN=${MAILER_DSN_PRESET:-null://null}"

if ssh ${SSH_KEY_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" "set -e; mkdir -p ${DEPLOY_PATH}; cd ${DEPLOY_PATH}; git clone ${GIT_REPO} . && mkdir -p var/log && cat > .env.local <<'ENVEOF'
${ENV_LOCAL_CONTENT}
ENVEOF
${COMPOSER_BIN} install --no-interaction --prefer-dist --no-progress --no-dev --no-scripts
bash bin/install-ffmpeg.sh || echo '⚠ ffmpeg non installé — vignettes vidéo indisponibles'
${PHP_BIN} bin/console cache:clear --env=prod
${PHP_BIN} bin/console assets:install public --env=prod
${PHP_BIN} bin/console importmap:install --env=prod
${PHP_BIN} bin/console doctrine:migrations:migrate --no-interaction --env=prod
${PHP_BIN} bin/console lexik:jwt:generate-keypair --skip-if-exists --env=prod
${PHP_BIN} bin/console asset-map:compile
echo '<!-- Deployed: '$(date '+%Y-%m-%d %H:%M:%S')' -->' > templates/deploy-info.html.twig
"; then
"; then :; else
error "${SUBDOMAIN} — échec à l'étape « clone + .env.local »"
RESULTS_FAIL+=("$SUBDOMAIN")
unset DB_PASSWORD_PRESET DB_PASSWORD APP_SECRET JWT_PASSPHRASE DATABASE_URL
continue
fi

if run_step "composer install" "${COMPOSER_BIN} install --no-interaction --prefer-dist --no-progress --no-dev --no-scripts" \
&& run_step "install-ffmpeg" "bash bin/install-ffmpeg.sh || echo '⚠ ffmpeg non installé — vignettes vidéo indisponibles'" \
&& run_step "cache:clear" "${PHP_BIN} bin/console cache:clear --env=prod" \
&& run_step "assets:install" "${PHP_BIN} bin/console assets:install public --env=prod" \
&& run_step "importmap:install" "${PHP_BIN} bin/console importmap:install --env=prod" \
&& run_step "migrations" "${PHP_BIN} bin/console doctrine:migrations:migrate --no-interaction --env=prod" \
&& run_step "jwt:generate-keypair" "${PHP_BIN} bin/console lexik:jwt:generate-keypair --skip-if-exists --env=prod" \
&& run_step "asset-map:compile" "${PHP_BIN} bin/console asset-map:compile" \
&& run_step "deploy-info" "echo '${DEPLOY_INFO_LINE}' > templates/deploy-info.html.twig"; then
success "${SUBDOMAIN} — déploiement initial OK"
RESULTS_OK+=("$SUBDOMAIN")
else
error "${SUBDOMAIN} — échec"
RESULTS_FAIL+=("$SUBDOMAIN")
fi

Expand All @@ -182,25 +203,20 @@ ENVEOF
var/tailwind/app.built.css \
"${SSH_USER}@${SSH_HOST}:${DEPLOY_PATH}/var/tailwind/app.built.css"

info "git pull + composer + cache + migrations + assets…"
if ssh ${SSH_KEY_OPTS} -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" "
set -e
cd ${DEPLOY_PATH}
mkdir -p var/log
git pull origin ${GIT_BRANCH}
${COMPOSER_BIN} install --no-interaction --prefer-dist --no-progress --no-dev --no-scripts
bash bin/install-ffmpeg.sh || echo '⚠ ffmpeg non installé — vignettes vidéo indisponibles'
${PHP_BIN} bin/console cache:clear --env=prod
${PHP_BIN} bin/console assets:install public --env=prod
${PHP_BIN} bin/console importmap:install --env=prod
${PHP_BIN} bin/console doctrine:migrations:migrate --no-interaction --env=prod
${PHP_BIN} bin/console asset-map:compile
echo '<!-- Deployed: '$(date '+%Y-%m-%d %H:%M:%S')' -->' > templates/deploy-info.html.twig
"; then
info "git pull + composer + cache + migrations + assets (étapes séparées)…"
DEPLOY_INFO_LINE="<!-- Deployed: $(date '+%Y-%m-%d %H:%M:%S') -->"
if run_step "git pull" "mkdir -p var/log && git pull origin ${GIT_BRANCH}" \
&& run_step "composer install" "${COMPOSER_BIN} install --no-interaction --prefer-dist --no-progress --no-dev --no-scripts" \
&& run_step "install-ffmpeg" "bash bin/install-ffmpeg.sh || echo '⚠ ffmpeg non installé — vignettes vidéo indisponibles'" \
&& run_step "cache:clear" "${PHP_BIN} bin/console cache:clear --env=prod" \
&& run_step "assets:install" "${PHP_BIN} bin/console assets:install public --env=prod" \
&& run_step "importmap:install" "${PHP_BIN} bin/console importmap:install --env=prod" \
&& run_step "migrations" "${PHP_BIN} bin/console doctrine:migrations:migrate --no-interaction --env=prod" \
&& run_step "asset-map:compile" "${PHP_BIN} bin/console asset-map:compile" \
&& run_step "deploy-info" "echo '${DEPLOY_INFO_LINE}' > templates/deploy-info.html.twig"; then
success "${SUBDOMAIN} — mise à jour OK"
RESULTS_OK+=("$SUBDOMAIN")
else
error "${SUBDOMAIN} — échec"
RESULTS_FAIL+=("$SUBDOMAIN")
fi
fi
Expand Down