171 lines
7.0 KiB
Bash
Executable File
171 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Déploie le build standalone de ce dépôt sur le LXC Homarr.
|
|
#
|
|
# Reproduit la disposition que le Dockerfile officiel assemble (ses COPY du
|
|
# stage `runner`), parce que l'installation du LXC en vient : /opt/homarr a la
|
|
# même forme qu'une image, sans le conteneur autour.
|
|
#
|
|
# Ce que le script NE touche PAS, volontairement :
|
|
# - /etc/homarr/homarr.env : secrets et configuration de l'instance
|
|
# - run.sh, entrypoint.sh, nginx.conf, redis.conf : modifiés par l'installateur
|
|
# du LXC, ils appartiennent à l'installation et non au build
|
|
# - /var/lib/homarr/db.sqlite : la base, sauvegardée mais jamais remplacée
|
|
#
|
|
# Usage : ops/deploy-lxc.sh [--dry-run]
|
|
#
|
|
set -euo pipefail
|
|
|
|
HOST="${HOMARR_HOST:-zachary@10.70.1.13}"
|
|
REMOTE_DIR="${HOMARR_REMOTE_DIR:-/opt/homarr}"
|
|
REMOTE_DB="${HOMARR_REMOTE_DB:-/var/lib/homarr/db.sqlite}"
|
|
SERVICE="${HOMARR_SERVICE:-homarr}"
|
|
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
DRY_RUN=""
|
|
[ "${1:-}" = "--dry-run" ] && DRY_RUN="--dry-run"
|
|
|
|
say() { printf '\n\033[1m==> %s\033[0m\n' "$1"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "Contrôles préalables"
|
|
|
|
cd "$REPO"
|
|
[ -z "$(git status --porcelain)" ] || {
|
|
echo "Arbre de travail sale. Committe ou remise avant de déployer." >&2
|
|
exit 1
|
|
}
|
|
echo "Commit déployé : $(git log --oneline -1)"
|
|
|
|
# Le dépôt exige un Node récent (champ engines.node). Le Node par défaut du poste
|
|
# est plus ancien que ça : sans ce contrôle, l'échec surviendrait au milieu du
|
|
# build, avec un message de pnpm qui ne dit pas quoi faire.
|
|
required_node="$(grep -oP '^node = "\K[^"]+' "$REPO/mise.toml" 2>/dev/null || echo '24.18.0')"
|
|
if ! node -e "process.exit(process.versions.node.localeCompare('$required_node', undefined, { numeric: true }) < 0 ? 1 : 0)"; then
|
|
echo "Node $(node -v) est trop ancien — il en faut au moins v$required_node." >&2
|
|
echo "Par exemple : export PATH=\"\$HOME/.local/share/nvm/v24.20.0/bin:\$PATH\"" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Une migration présente ici et absente en prod n'est pas bloquante — Homarr les
|
|
# applique au démarrage — mais elle rend le rollback non trivial : restaurer
|
|
# l'archive ne défait pas un schéma déjà migré. On veut le savoir AVANT.
|
|
#
|
|
# Comparaison par dialecte : la prod embarque les migrations de tous les moteurs,
|
|
# et prendre le maximum toutes dialectes confondues masquerait une divergence
|
|
# propre à celui qui tourne réellement.
|
|
DIALECT="${HOMARR_DB_DIALECT:-sqlite}"
|
|
local_migration="$(find "packages/db/migrations/$DIALECT" -name '*.sql' -printf '%f\n' | sort | tail -1)"
|
|
remote_migration="$(ssh "$HOST" "find $REMOTE_DIR/db/migrations/$DIALECT -name '*.sql' -printf '%f\n' | sort | tail -1")"
|
|
echo "Dernière migration — locale : $local_migration | prod : $remote_migration"
|
|
if [ "$local_migration" != "$remote_migration" ]; then
|
|
echo
|
|
echo "!! Les migrations divergent. Le rollback ne sera PAS une simple restauration."
|
|
echo " Sauvegarde la base avant de continuer, puis relance en connaissance de cause."
|
|
read -r -p " Continuer quand même ? [oui/NON] " answer
|
|
[ "$answer" = "oui" ] || exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "Build"
|
|
|
|
pnpm turbo build --filter=@homarr/nextjs... --filter=@homarr/cli
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "Assemblage de l'arborescence à envoyer"
|
|
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
|
|
# L'ordre suit le Dockerfile : le standalone d'abord, le reste par-dessus.
|
|
cp -a apps/nextjs/.next/standalone/. "$STAGE/"
|
|
mkdir -p "$STAGE/apps/nextjs/.next"
|
|
cp -a apps/nextjs/.next/static "$STAGE/apps/nextjs/.next/static"
|
|
cp -a apps/nextjs/public "$STAGE/apps/nextjs/public"
|
|
cp -a apps/nextjs/next.config.ts apps/nextjs/package.json pnpm-workspace.yaml "$STAGE/"
|
|
|
|
mkdir -p "$STAGE/apps/cli"
|
|
cp -a packages/cli/cli.cjs "$STAGE/apps/cli/cli.cjs"
|
|
|
|
# Binaire natif : compilé ici, chargé là-bas. Même architecture et même ABI Node
|
|
# (24.x des deux côtés) sont donc des prérequis — le contrôle est plus bas.
|
|
mkdir -p "$STAGE/build"
|
|
cp -a node_modules/better-sqlite3/build/Release/better_sqlite3.node "$STAGE/build/"
|
|
|
|
mkdir -p "$STAGE/db"
|
|
cp -a packages/db/migrations "$STAGE/db/migrations"
|
|
|
|
echo "Arborescence prête : $(du -sh "$STAGE" | cut -f1)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "Contrôle de compatibilité du binaire natif"
|
|
|
|
local_abi="$(node -p 'process.versions.modules')"
|
|
remote_abi="$(ssh "$HOST" 'node -p "process.versions.modules"')"
|
|
local_arch="$(uname -m)"
|
|
remote_arch="$(ssh "$HOST" 'uname -m')"
|
|
echo "ABI Node — local : $local_abi | prod : $remote_abi"
|
|
echo "Arch — local : $local_arch | prod : $remote_arch"
|
|
if [ "$local_abi" != "$remote_abi" ] || [ "$local_arch" != "$remote_arch" ]; then
|
|
echo "!! better_sqlite3.node ne se chargera pas en prod. Build sur le LXC, ou aligne Node." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ -n "$DRY_RUN" ]; then
|
|
say "Mode --dry-run : arrêt avant toute modification de la prod"
|
|
rsync -an --delete \
|
|
--exclude 'run.sh' --exclude 'entrypoint.sh' \
|
|
--exclude 'nginx.conf' --exclude 'redis.conf' \
|
|
"$STAGE/" "$HOST:$REMOTE_DIR/" | head -40
|
|
exit 0
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "Sauvegarde"
|
|
|
|
stamp="$(date +%F-%H%M)"
|
|
ssh -t "$HOST" "sudo tar czf /root/homarr-backup-$stamp.tar.gz $REMOTE_DIR \
|
|
&& sudo cp $REMOTE_DB /root/homarr-db-$stamp.sqlite \
|
|
&& sudo ls -lh /root/homarr-backup-$stamp.tar.gz /root/homarr-db-$stamp.sqlite"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "Arrêt du service"
|
|
|
|
# Jamais de rsync sur un Next.js en cours d'exécution : il lit ses chunks à la
|
|
# demande et servirait des morceaux de deux builds différents.
|
|
ssh -t "$HOST" "sudo systemctl stop $SERVICE && ! systemctl is-active --quiet $SERVICE && echo arrêté"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "Envoi"
|
|
|
|
# --delete retire ce que le build ne produit plus ; les quatre fichiers exclus
|
|
# sont protégés de cette suppression par leur exclusion même.
|
|
rsync -a --delete --rsync-path="sudo rsync" \
|
|
--exclude 'run.sh' --exclude 'entrypoint.sh' \
|
|
--exclude 'nginx.conf' --exclude 'redis.conf' \
|
|
"$STAGE/" "$HOST:$REMOTE_DIR/"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "Redémarrage"
|
|
|
|
ssh -t "$HOST" "sudo systemctl start $SERVICE"
|
|
sleep 10
|
|
ssh "$HOST" "systemctl is-active $SERVICE; curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1/"
|
|
|
|
say "Journaux (40 dernières lignes)"
|
|
ssh -t "$HOST" "sudo journalctl -u $SERVICE -n 40 --no-pager"
|
|
|
|
cat <<EOF
|
|
|
|
Déployé. Vérifie dans le navigateur que les intégrations PRÉEXISTANTES
|
|
fonctionnent encore — c'est ce qui prouve que SECRET_ENCRYPTION_KEY est intact.
|
|
|
|
Retour arrière :
|
|
ssh $HOST 'sudo systemctl stop $SERVICE \\
|
|
&& sudo rm -rf $REMOTE_DIR \\
|
|
&& sudo tar xzf /root/homarr-backup-$stamp.tar.gz -C / \\
|
|
&& sudo systemctl start $SERVICE'
|
|
|
|
La base n'a pas été remplacée ; sa copie est dans /root/homarr-db-$stamp.sqlite.
|
|
EOF
|