Last active
July 12, 2026 09:59
-
-
Save kibotu/3b55148c2c2a58fb1ddbd90dd66380cc to your computer and use it in GitHub Desktop.
Install LTS on RaspberryPi 5 aarch64
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # install-node.sh | |
| # Installs the latest Node.js Active LTS release (official arm64 build) | |
| # for 64-bit Raspberry Pi OS / Debian aarch64 (e.g. Raspberry Pi 5, Pi 4 64-bit, Pi 400, etc). | |
| # | |
| # Originally based on a script by Steven de Salas, itself based on | |
| # Richard Stanley's Node-MongoDb-Pi script for the armv6l Pi Zero. | |
| # Rewritten for 64-bit Pi hardware using official nodejs.org arm64 builds | |
| # (the unofficial-builds armv6l tarball is no longer needed on 64-bit Pis). | |
| # | |
| # Usage: | |
| # ./install-node.sh # installs latest Active LTS | |
| # NODE_VERSION=v22.20.0 ./install-node.sh # pin a specific version | |
| set -euo pipefail | |
| INSTALL_ROOT="/opt/nodejs" | |
| BIN_DIR="/usr/local/bin" | |
| TMP_DIR="$(mktemp -d /tmp/node-install.XXXXXX)" | |
| log() { printf '\033[1;34m[node-install]\033[0m %s\n' "$*"; } | |
| err() { printf '\033[1;31m[node-install]\033[0m %s\n' "$*" >&2; } | |
| cleanup() { rm -rf "$TMP_DIR"; } | |
| trap cleanup EXIT | |
| # --------------------------------------------------------------------------- | |
| # 0. Sanity checks | |
| # --------------------------------------------------------------------------- | |
| if [[ $EUID -eq 0 ]]; then | |
| err "Please run this as a normal user (it will sudo when needed), not as root directly." | |
| exit 1 | |
| fi | |
| for cmd in curl tar sudo python3; do | |
| if ! command -v "$cmd" >/dev/null 2>&1; then | |
| err "Required command '$cmd' not found. Install it first, e.g.: sudo apt update && sudo apt install -y $cmd" | |
| exit 1 | |
| fi | |
| done | |
| ARCH_RAW="$(uname -m)" | |
| case "$ARCH_RAW" in | |
| aarch64|arm64) | |
| NODE_ARCH="arm64" | |
| ;; | |
| armv7l) | |
| NODE_ARCH="armv7l" | |
| ;; | |
| armv6l) | |
| err "armv6l (32-bit Pi Zero/1) detected. Official Node.js no longer ships this build;" | |
| err "you need the unofficial-builds.nodejs.org armv6l tarball instead of this script." | |
| exit 1 | |
| ;; | |
| x86_64) | |
| NODE_ARCH="x64" | |
| ;; | |
| *) | |
| err "Unsupported architecture: $ARCH_RAW" | |
| exit 1 | |
| ;; | |
| esac | |
| log "Detected architecture: $ARCH_RAW -> node arch '$NODE_ARCH'" | |
| # --------------------------------------------------------------------------- | |
| # 1. Resolve the version to install | |
| # --------------------------------------------------------------------------- | |
| if [[ -z "${NODE_VERSION:-}" ]]; then | |
| log "No NODE_VERSION set, looking up latest Active LTS release..." | |
| # Download the full release index to a file first (rather than piping straight | |
| # into grep/head), so curl always finishes writing and can't be killed by | |
| # SIGPIPE from a downstream reader closing early (which curl reports as exit 23). | |
| INDEX_FILE="$TMP_DIR/index.json" | |
| curl -fsSL --retry 3 -o "$INDEX_FILE" https://nodejs.org/dist/index.json | |
| # Parse with python3's json module rather than grep/sed: the index's exact | |
| # formatting (single-line vs pretty-printed) isn't something to rely on, and | |
| # a real parser handles it regardless. The index is newest-first, and each | |
| # LTS release has "lts" set to its codename string; Current (non-LTS) | |
| # releases have "lts": false. | |
| NODE_VERSION="$( | |
| python3 - "$INDEX_FILE" <<'PYEOF' | |
| import json, sys | |
| with open(sys.argv[1]) as f: | |
| releases = json.load(f) | |
| for r in releases: | |
| if r.get("lts"): | |
| print(r["version"]) | |
| break | |
| PYEOF | |
| )" | |
| if [[ -z "$NODE_VERSION" ]]; then | |
| err "Could not auto-detect the latest LTS version. Set NODE_VERSION manually, e.g.:" | |
| err " NODE_VERSION=v24.18.0 $0" | |
| exit 1 | |
| fi | |
| fi | |
| log "Target Node.js version: $NODE_VERSION" | |
| TARBALL="node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" | |
| BASE_URL="https://nodejs.org/dist/${NODE_VERSION}" | |
| DOWNLOAD_URL="${BASE_URL}/${TARBALL}" | |
| SHASUMS_URL="${BASE_URL}/SHASUMS256.txt" | |
| # --------------------------------------------------------------------------- | |
| # 2. Skip if already installed | |
| # --------------------------------------------------------------------------- | |
| if command -v node >/dev/null 2>&1; then | |
| CURRENT_VERSION="$(node --version || true)" | |
| if [[ "$CURRENT_VERSION" == "$NODE_VERSION" ]]; then | |
| log "Node.js $NODE_VERSION is already installed at $(command -v node). Nothing to do." | |
| exit 0 | |
| fi | |
| log "Found existing Node.js $CURRENT_VERSION, will replace with $NODE_VERSION." | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # 3. Download + verify + extract | |
| # --------------------------------------------------------------------------- | |
| log "Downloading $DOWNLOAD_URL" | |
| curl -fsSL --retry 3 -o "$TMP_DIR/$TARBALL" "$DOWNLOAD_URL" | |
| log "Verifying checksum" | |
| curl -fsSL --retry 3 -o "$TMP_DIR/SHASUMS256.txt" "$SHASUMS_URL" | |
| EXPECTED_SHA="$(grep " ${TARBALL}\$" "$TMP_DIR/SHASUMS256.txt" | awk '{print $1}')" | |
| if [[ -z "$EXPECTED_SHA" ]]; then | |
| err "Could not find checksum entry for $TARBALL - aborting for safety." | |
| exit 1 | |
| fi | |
| ACTUAL_SHA="$(sha256sum "$TMP_DIR/$TARBALL" | awk '{print $1}')" | |
| if [[ "$EXPECTED_SHA" != "$ACTUAL_SHA" ]]; then | |
| err "Checksum mismatch! Expected $EXPECTED_SHA, got $ACTUAL_SHA. Aborting." | |
| exit 1 | |
| fi | |
| log "Checksum OK" | |
| log "Extracting" | |
| tar -xf "$TMP_DIR/$TARBALL" -C "$TMP_DIR" | |
| EXTRACTED_DIR="$TMP_DIR/node-${NODE_VERSION}-linux-${NODE_ARCH}" | |
| # --------------------------------------------------------------------------- | |
| # 4. Install into /opt, versioned, with a stable "current" symlink | |
| # --------------------------------------------------------------------------- | |
| VERSIONED_TARGET="${INSTALL_ROOT}-${NODE_VERSION}" | |
| log "Installing to $VERSIONED_TARGET" | |
| sudo rm -rf "$VERSIONED_TARGET" | |
| sudo mv "$EXTRACTED_DIR" "$VERSIONED_TARGET" | |
| log "Updating $INSTALL_ROOT symlink -> $VERSIONED_TARGET" | |
| sudo ln -sfn "$VERSIONED_TARGET" "$INSTALL_ROOT" | |
| # --------------------------------------------------------------------------- | |
| # 5. Symlink binaries onto PATH | |
| # --------------------------------------------------------------------------- | |
| log "Linking binaries into $BIN_DIR" | |
| for bin in node npm npx corepack; do | |
| if [[ -e "$INSTALL_ROOT/bin/$bin" ]]; then | |
| sudo ln -sf "$INSTALL_ROOT/bin/$bin" "$BIN_DIR/$bin" | |
| fi | |
| done | |
| # --------------------------------------------------------------------------- | |
| # 6. Verify | |
| # --------------------------------------------------------------------------- | |
| hash -r | |
| log "Installed:" | |
| node --version | |
| npm --version | |
| log "Done. Node.js ${NODE_VERSION} is installed at ${VERSIONED_TARGET}, symlinked via ${INSTALL_ROOT} and ${BIN_DIR}." | |
| log "To roll back, re-run with NODE_VERSION=<old-version>, or manually repoint the ${INSTALL_ROOT} symlink." |
kibotu
commented
Jul 12, 2026
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment