Skip to content

Instantly share code, notes, and snippets.

@chrisdc
Last active April 5, 2026 15:51
Show Gist options
  • Select an option

  • Save chrisdc/eba88d1216304fd2eac6dfc1e2e0935c to your computer and use it in GitHub Desktop.

Select an option

Save chrisdc/eba88d1216304fd2eac6dfc1e2e0935c to your computer and use it in GitHub Desktop.
Rasperry Pi Setup
#!/usr/bin/env bash
# =============================================================================
# Raspberry Pi Setup Script
# Installs: nvm, Node.js LTS, uv, git (with config), SSH key for GitHub, VNC
# =============================================================================
set -euo pipefail
# --- Flags -------------------------------------------------------------------
INSTALL_VNC=false
for arg in "$@"; do
case "$arg" in
--vnc) INSTALL_VNC=true ;;
--help|-h)
echo "Usage: $0 [--vnc]"
echo " --vnc Also enable VNC remote desktop"
exit 0 ;;
*) echo "Unknown option: $arg (use --help for usage)" >&2; exit 1 ;;
esac
done
# --- Colours -----------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
# --- Helpers -----------------------------------------------------------------
info() { echo -e "${CYAN}[INFO]${RESET} $*"; }
success() { echo -e "${GREEN}[OK]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; }
section() { echo -e "\n${BOLD}${CYAN}==> $*${RESET}"; }
prompt() {
# prompt <var_name> <display_text> [default]
local var="$1"
local msg="$2"
local default="${3:-}"
local input
if [[ -n "$default" ]]; then
read -rp "$(echo -e "${YELLOW}?${RESET} ${msg} [${default}]: ")" input
printf -v "$var" '%s' "${input:-$default}"
else
while true; do
read -rp "$(echo -e "${YELLOW}?${RESET} ${msg}: ")" input
[[ -n "$input" ]] && break
warn "This field is required."
done
printf -v "$var" '%s' "$input"
fi
}
# =============================================================================
# 0. Banner
# =============================================================================
echo -e "${BOLD}"
echo " ______ _____ _____ _____ _ "
echo " | ____| __ \_ _| / ____| | | "
echo " | |__ | |__) || | | (___ ___| |_ _ _ _ __ "
echo " | __| | ___/ | | \___ \ / _ \ __| | | | '_ \ "
echo " | | | | _| |_ ____) | __/ |_| |_| | |_) |"
echo " |_| |_| |_____||_____/ \___|\__|\__,_| .__/ "
echo " | | "
echo " |_| "
echo -e "${RESET}"
echo -e " Raspberry Pi First-Boot Setup — $(date '+%Y-%m-%d')\n"
# =============================================================================
# 1. Collect user input up front
# =============================================================================
section "Configuration"
# printf -v (used inside prompt) creates variables dynamically, which trips
# set -u before the variable is first read. Disable it for this block.
set +u
prompt GIT_NAME "Git full name (e.g. Jane Smith)"
prompt GIT_EMAIL "Git email address"
prompt SSH_KEY_FILE "SSH key filename" "$HOME/.ssh/id_ed25519"
prompt SSH_KEY_COMMENT "SSH key comment/label" "$GIT_EMAIL"
# Optional passphrase (hidden input)
read -rsp "$(echo -e "${YELLOW}?${RESET} SSH key passphrase (leave blank for none): ")" SSH_PASSPHRASE
echo
set -u
echo
info "Configuration collected. Starting installation…"
# =============================================================================
# 2. System update & base dependencies
# =============================================================================
section "System packages"
info "Updating package lists…"
sudo apt-get update -qq
info "Installing base dependencies (curl, git, build-essential)…"
sudo apt-get install -y -qq curl git build-essential ca-certificates
success "Base dependencies ready."
# =============================================================================
# 3. nvm + Node.js LTS
# =============================================================================
section "nvm & Node.js LTS"
NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
if [[ -d "$NVM_DIR" ]]; then
warn "nvm already installed at $NVM_DIR — skipping nvm install."
else
info "Installing nvm…"
NVM_INSTALL_URL="https://raw.githubusercontent.com/nvm-sh/nvm/HEAD/install.sh"
curl -fsSL "$NVM_INSTALL_URL" | bash
success "nvm installed."
fi
# Load nvm into the current shell.
# nvm.sh has unset variables that trip set -u, so disable it temporarily.
set +u
export NVM_DIR="$NVM_DIR"
# shellcheck source=/dev/null
[[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh"
[[ -s "$NVM_DIR/bash_completion" ]] && source "$NVM_DIR/bash_completion"
info "Installing latest Node.js LTS…"
nvm install --lts
nvm use --lts
nvm alias default 'lts/*'
NODE_VER=$(node --version)
NPM_VER=$(npm --version)
set -u
success "Node $NODE_VER and npm $NPM_VER active."
# Persist nvm in shell config files that may not yet have it
for RC_FILE in "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do
if [[ -f "$RC_FILE" ]] && ! grep -q 'NVM_DIR' "$RC_FILE"; then
cat >> "$RC_FILE" <<'EOF'
# nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
EOF
info "Added nvm init to $RC_FILE"
fi
done
# =============================================================================
# 4. uv (Python package manager)
# =============================================================================
section "uv package manager"
if command -v uv &>/dev/null; then
warn "uv is already installed ($(uv --version)) — skipping."
else
info "Installing uv…"
curl -fsSL https://astral.sh/uv/install.sh | sh
# uv installs to ~/.cargo/bin or ~/.local/bin depending on version
# Add both to PATH for this session
export PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH"
success "uv $(uv --version) installed."
fi
# Ensure PATH addition is persisted
for RC_FILE in "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do
if [[ -f "$RC_FILE" ]] && ! grep -q 'uv' "$RC_FILE"; then
echo 'export PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH"' >> "$RC_FILE"
info "Added uv PATH to $RC_FILE"
fi
done
# Configure uv to use system site-packages by default (required for Pi camera)
info "Configuring uv to use system site-packages (for PiCamera2 / libcamera)…"
UV_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/uv"
mkdir -p "$UV_CONFIG_DIR"
UV_CONFIG_FILE="$UV_CONFIG_DIR/uv.toml"
if [[ ! -f "$UV_CONFIG_FILE" ]]; then
cat > "$UV_CONFIG_FILE" <<'EOF'
[venv]
system-site-packages = true
EOF
success "Created $UV_CONFIG_FILE with system-site-packages = true"
else
if grep -q 'system-site-packages' "$UV_CONFIG_FILE"; then
warn "$UV_CONFIG_FILE already contains system-site-packages — leaving untouched."
else
echo -e '\n[venv]\nsystem-site-packages = true' >> "$UV_CONFIG_FILE"
success "Appended system-site-packages = true to $UV_CONFIG_FILE"
fi
fi
info "Verifying: uv venv --system-site-packages creates correct environment…"
TMPDIR_UV=$(mktemp -d)
uv venv --system-site-packages "$TMPDIR_UV/test_env" &>/dev/null && \
success "uv venv --system-site-packages works correctly." || \
warn "Could not verify uv venv — check manually after reboot."
rm -rf "$TMPDIR_UV"
# =============================================================================
# 5. Git configuration
# =============================================================================
section "Git configuration"
info "Setting git user.name → $GIT_NAME"
git config --global user.name "$GIT_NAME"
info "Setting git user.email → $GIT_EMAIL"
git config --global user.email "$GIT_EMAIL"
# Sensible defaults
git config --global init.defaultBranch main
git config --global pull.rebase false
git config --global core.autocrlf input
success "Git configured."
git config --global --list | grep -E '^user\.' | while IFS= read -r line; do
info " $line"
done
# =============================================================================
# 6. SSH key for GitHub
# =============================================================================
section "SSH key for GitHub"
SSH_DIR="$(dirname "$SSH_KEY_FILE")"
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
if [[ -f "$SSH_KEY_FILE" ]]; then
warn "SSH key already exists at $SSH_KEY_FILE — skipping key generation."
else
info "Generating Ed25519 SSH key…"
ssh-keygen -t ed25519 \
-C "$SSH_KEY_COMMENT" \
-f "$SSH_KEY_FILE" \
-N "$SSH_PASSPHRASE"
success "SSH key created: $SSH_KEY_FILE"
fi
# Ensure ssh-agent entry in shell config
for RC_FILE in "$HOME/.bashrc" "$HOME/.bash_profile"; do
if [[ -f "$RC_FILE" ]] && ! grep -q 'ssh-agent' "$RC_FILE"; then
cat >> "$RC_FILE" <<EOF
# Start ssh-agent automatically
if ! pgrep -u "\$USER" ssh-agent >/dev/null 2>&1; then
eval "\$(ssh-agent -s)" >/dev/null
fi
ssh-add "$SSH_KEY_FILE" 2>/dev/null || true
EOF
info "Added ssh-agent auto-start to $RC_FILE"
fi
done
# Start ssh-agent now and add key for this session
eval "$(ssh-agent -s)" >/dev/null 2>&1 || true
ssh-add "$SSH_KEY_FILE" 2>/dev/null || true
# =============================================================================
# 7. Add public key to GitHub
# =============================================================================
section "Add SSH key to GitHub"
PUB_KEY_FILE="${SSH_KEY_FILE}.pub"
PUB_KEY_CONTENT=$(cat "$PUB_KEY_FILE")
echo
echo -e "${BOLD}Your public SSH key (add this to GitHub):${RESET}"
echo -e "${YELLOW}────────────────────────────────────────────────────────────${RESET}"
echo "$PUB_KEY_CONTENT"
echo -e "${YELLOW}────────────────────────────────────────────────────────────${RESET}"
echo
echo -e " 1. Go to ${CYAN}https://github.com/settings/ssh/new${RESET}"
echo -e " 2. Paste the key above into the 'Key' field."
echo -e " 3. Give it a memorable title (e.g. 'Raspberry Pi')."
echo -e " 4. Click ${BOLD}Add SSH key${RESET}."
echo
read -rp "$(echo -e "${YELLOW}?${RESET} Press Enter once you have added the key to GitHub… ")"
info "Testing SSH connection to GitHub…"
if ssh -o StrictHostKeyChecking=accept-new -T git@github.com 2>&1 | grep -q 'successfully authenticated'; then
success "GitHub SSH authentication successful!"
else
warn "Could not confirm GitHub auth automatically."
info "Run ${BOLD}ssh -T git@github.com${RESET} after reboot to verify."
fi
# =============================================================================
# 8. VNC (optional — pass --vnc to enable)
# =============================================================================
if [[ "$INSTALL_VNC" == true ]]; then
section "VNC remote desktop"
# Install RealVNC server (included in Raspberry Pi OS but may be missing on
# lite images) plus the virtual framebuffer needed for headless operation.
info "Installing RealVNC server and virtual framebuffer…"
sudo apt-get install -y -qq realvnc-vnc-server realvnc-vnc-viewer xvfb || {
warn "RealVNC packages not found in apt — trying tigervnc as fallback…"
sudo apt-get install -y -qq tigervnc-standalone-server tigervnc-common xvfb
}
# Enable and start the vncserver-x11-serviced service (RealVNC)
if systemctl list-unit-files | grep -q 'vncserver-x11-serviced'; then
info "Enabling vncserver-x11-serviced (RealVNC service mode)…"
sudo systemctl enable vncserver-x11-serviced
sudo systemctl start vncserver-x11-serviced
success "RealVNC service enabled and started."
else
warn "vncserver-x11-serviced unit not found — VNC may need manual configuration."
fi
# Enable VNC via raspi-config (works on Raspberry Pi OS)
if command -v raspi-config &>/dev/null; then
info "Enabling VNC via raspi-config…"
sudo raspi-config nonint do_vnc 0 # 0 = enable
success "VNC enabled via raspi-config."
else
warn "raspi-config not found — skipping automated VNC enable."
info "If running Raspberry Pi OS Desktop, you can enable VNC manually:"
info " Preferences → Raspberry Pi Configuration → Interfaces → VNC: Enabled"
fi
# Display connection info
PI_IP=$(hostname -I | awk '{print $1}')
echo
echo -e "${BOLD}VNC connection details:${RESET}"
echo -e " Address : ${CYAN}${PI_IP}:5900${RESET} (or use hostname: ${CYAN}$(hostname).local:5900${RESET})"
echo -e " Client : RealVNC Viewer — ${CYAN}https://www.realvnc.com/en/connect/download/viewer/${RESET}"
echo -e " Login : your Pi username / password"
echo
fi # --vnc
# =============================================================================
# 9. Summary
# =============================================================================
section "Setup complete!"
echo -e "${GREEN}${BOLD}"
echo " ✔ nvm $(nvm --version 2>/dev/null || echo '(reload shell)')"
echo " ✔ Node.js $(node --version 2>/dev/null || echo '(reload shell)')"
echo " ✔ npm $(npm --version 2>/dev/null || echo '(reload shell)')"
echo " ✔ uv $(uv --version 2>/dev/null || echo '(reload shell)')"
echo " ✔ git $(git --version)"
echo " ✔ SSH key $SSH_KEY_FILE"
if [[ "$INSTALL_VNC" == true ]]; then
echo " ✔ VNC enabled (port 5900)"
fi
echo -e "${RESET}"
warn "Reload your shell (or run ${BOLD}source ~/.bashrc${RESET} ) to activate all PATH changes."
echo
@chrisdc
Copy link
Copy Markdown
Author

chrisdc commented Apr 5, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment