Aller au contenu

Déployer chez un hébergeur client

Cette procédure concerne les sites développés sur le VPS Levell mais hébergés chez le client — le cas d’onas.sn chez Infomaniak. Pour les applications qui tournent sur notre propre VPS, voir plutôt Déployer une nouvelle application.


Trois idées, dont découle tout le reste.

L’artefact est autonome. Next.js sait produire une sortie standalone : un serveur complet accompagné des seules dépendances réellement utilisées. Compilé dans une image dont la bibliothèque système correspond à celle du serveur cible, il n’exige aucun npm install chez le client — donc aucun shell, aucun pic de mémoire, aucune dépendance de développement manquante.

Les données du client sont hors d’atteinte, par construction. La base, les médias du back-office et le fichier .env sont déclarés dans une liste PROTECTED. Ce n’est pas une exclusion de précaution : l’archive est relue avant tout téléversement et le script s’interrompt si l’un de ces chemins s’y trouve.

La bascule est réversible. L’ancien .next est déplacé dans une sauvegarde horodatée, le nouveau prend sa place par un simple mv — instantané, puisqu’on reste sur le même système de fichiers. Le retour arrière refait l’opération en sens inverse.


/home/sidy/deploy-kit/
├── deploy.sh # le code
├── migrate-db.sh # les changements de schéma
├── lib/remote.sh # transport (scp + ssh -tt)
├── clients/<nom>.conf # configuration, versionnée
├── clients/<nom>.secret # mot de passe, chmod 600, JAMAIS versionné
├── artifacts/ # artefacts compilés
└── backups/ # bases rapatriées
Fenêtre de terminal
CLIENT_NAME="ONAS"
SOURCE_REPO="/docker/onas-web"
REMOTE_HOST="<hôte-ssh-du-client>"
REMOTE_USER="<utilisateur-ssh>"
REMOTE_APP="/srv/customer/web" # chemin figé par le panneau de l'hébergeur
REMOTE_WORK="/srv/customer/.deploy" # sauvegardes + transit
BUILD_IMAGE="node:24-bookworm-slim" # DOIT correspondre à la glibc du serveur cible
PROTECTED=( "data" "public/media" ".env" ) # garde-fou — ne jamais vider
STABLE=( "public/images" ) # lourd et stable, envoyé sur --with-images
HEALTH_BASE="https://onas.sn"
RESTART_MODE="manual"

Ne jamais s’engager sur un hébergement qu’on n’a pas mesuré. Le relevé se fait en lecture seule et prend cinq minutes :

Fenêtre de terminal
cat /etc/os-release | head -2 # distribution
ldd --version | head -1 # ← LA donnée qui conditionne l'image de build
node -v ; npm -v
find / -maxdepth 6 -name "next.config.*" -not -path "*/node_modules/*" 2>/dev/null
df -h . ; free -m ; nproc
for t in rsync git tar curl unzip; do command -v $t; done

La version de glibc détermine BUILD_IMAGE. Un artefact compilé sur une glibc plus récente que celle du serveur ne démarrera pas : les binaires natifs (sharp, @libsql/client) sont incompatibles.

Serveur cibleBUILD_IMAGE
Debian 12 « bookworm », glibc 2.36node:24-bookworm-slim
Debian 11 « bullseye », glibc 2.31node:24-bullseye-slim
Alpine, muslnode:24-alpine

Fenêtre de terminal
cd /home/sidy/deploy-kit
./deploy.sh <client>

Le script enchaîne : compilation de l’artefact → assemblage de l’archive → vérification que l’archive ne contient aucune donnée client → contrôles préalables sur le serveur → téléversement → sauvegarde → bascule → contrôle de santé.

OptionEffet
--dry-runTout sauf le téléversement. À utiliser sans hésiter.
--no-buildRéutilise le dernier artefact compilé.
--with-imagesEnvoie aussi public/images. Obligatoire si les images ont changé.
--health-onlyNe fait que le contrôle de santé.
--rollbackRestaure une sauvegarde antérieure.
--no-healthS’arrête après la mise en place (le contrôle n’a de sens qu’après redémarrage).
--yesSans confirmation.

Sur un hébergement mutualisé de type « site Node.js », la console SSH tourne dans un conteneur distinct de celui qui exécute l’application — les deux ne partagent que le disque. Aucun processus applicatif n’y est visible, ni pm2, ni systemctl : le redémarrage passe obligatoirement par le panneau de l’hébergeur.

Une astuce évite d’avoir à faire reconfigurer quoi que ce soit : l’artefact embarque "start": "node server.js" dans son package.json. Le panneau continue de lancer npm start, sans savoir que le contenu a changé.

Fenêtre de terminal
./deploy.sh <client> --no-health # met en place, s'arrête proprement
# → Panneau de l'hébergeur → Node.js → Redémarrer
./deploy.sh <client> --health-only # vérifie

L’artefact autonome n’embarque ni src/migrations/ ni tsx : la migration ne peut pas s’exécuter chez le client. On rapatrie sa base, on la migre ici, on la renvoie.

Fenêtre de terminal
./migrate-db.sh <client> --dry-run # répétition sur ses données réelles
./migrate-db.sh <client> # pour de vrai
  1. La base d’origine est sauvegardée avant toute opération, horodatée, en lecture seule (chmod 444), et jamais écrasée par une exécution ultérieure.
  2. La migration s’applique sur une copie.
  3. Le nombre de lignes de chaque table est compté avant et après. La moindre perte interrompt tout — le renvoi n’a pas lieu.
  4. Une sauvegarde est également déposée sur le serveur du client avant le remplacement.

Toujours migrer avant de déployer. Le code lit les nouvelles tables mais retombe sur un contenu de repli si elles sont absentes — l’inverse (migrer sans déployer encore) est également sans danger : l’ancien code ignore simplement les tables qu’il ne connaît pas.


Fenêtre de terminal
./deploy.sh <client> --rollback # liste les sauvegardes, en restaure une

Puis redémarrer. Pour la base, la sauvegarde est sur le serveur du client :

Fenêtre de terminal
cp /srv/customer/.deploy/backups/db/payload-<horodatage>.db \
/srv/customer/web/data/payload.db

Ces contraintes ont été mesurées sur Infomaniak, mais se retrouvent ailleurs.

ContrainteConséquence
Canal SSH exec refuséssh hôte 'commande' sort en 255 sans rien renvoyer. ssh -tt fonctionne : on téléverse le script, puis on ne tape qu’une ligne (bash script) — c’est ce qui rend $? exploitable, car dans un pseudo-terminal $? après plusieurs lignes ne désigne que la dernière.
Sous-système SFTP refusé« Permission denied » quel que soit le mode d’authentification. Seul scp passe.
rsync inutilisableIl a besoin d’exec. Tout passe donc par une archive unique en scp.
Authentification par clé indisponibleInfomaniak l’annonce dans sa bannière (« will be supported soon »). Mot de passe obligatoire, à faire renouveler régulièrement.
Redémarrage non scriptableConteneur distinct — panneau obligatoire.
Bannière au loginElle avale l’entrée envoyée trop tôt : temporiser avant d’écrire dans le pseudo-terminal.

8. Pièges vérifiés — et comment les reconnaître

Section intitulée « 8. Pièges vérifiés — et comment les reconnaître »
SymptômeCauseCorrection
Failed query: CREATE INDEX …_idx pendant la migrationNODE_ENV=development déclenche une synchronisation automatique de schéma avant la migration, qui tente de recréer des index existantsNODE_ENV=production
sh: 1: next: not foundArtefact non autonome, node_modules absentCompiler avec output: 'standalone'
Le site sert encore l’ancienne versionApplication non redémarréePanneau → Redémarrer
Toutes les images repassent lentement après chaque mise en ligne.next/cache/images détruit par la basculedeploy.sh reporte ce cache — vérifier la ligne « cache d’images conservé »
Module not found sur src/data/… à la compilationMotif rsync non ancré : --exclude data exclut aussi src/data/Ancrer : --exclude /data
« ancien build encore servi » alors que tout est correctAvec Turbopack, les chunks sont nommés par empreinte de contenu : le BUILD_ID n’y figure pasInterroger /_next/static/<BUILD_ID>/_buildManifest.js (200 = en ligne)

Deux méthodes indépendantes, à privilégier sur toute déduction :

Fenêtre de terminal
# 1. Le manifeste du build attendu répond-il ?
curl -s -o /dev/null -w '%{http_code}\n' \
https://<site>/_next/static/<BUILD_ID>/_buildManifest.js # 200 attendu
# 2. Identité d'octets d'un chunk servi
C=$(curl -s https://<site>/ | grep -oE '/_next/static/chunks/[A-Za-z0-9_.~-]+\.js' | head -1)
curl -s "https://<site>$C" | md5sum
md5sum /home/sidy/deploy-kit/artifacts/<client>/build/.next${C#/_next}

Déploiement du 13 août 2026, BUILD_ID 8sv-VvFvWXYTTg1niNi9H.

AvantAprès
node_modules sur le serveur818 Mo53 Mo
public/images277 Mo82 Mo
Archive transférée1,27 Go (livraison manuelle)108 Mo, 3 s
Images de la page d’accueil166 variantes, 15,27 Mo152 variantes, 9,33 Mo
npm install chez le clientobligatoireaucun

Base, médias et .env vérifiés intacts après bascule. Migration de schéma appliquée sans perdre une ligne sur 34 tables.

Séquence exécutée :

Fenêtre de terminal
cd /home/sidy/deploy-kit
./migrate-db.sh onas # ~1 min
./deploy.sh onas --with-images --no-health # 3 s de transfert
# → Manager Infomaniak → Node.js → Redémarrer
./deploy.sh onas --health-only # tout au vert