Back to Config Library
Dev Asset
v2.0.0

Ubuntu Bootstrap Script

340-line idempotent VM setup — Zsh, Node.js, Docker, 35+ tools, dotfiles checkout, all in one command.

Files Included

339 linesEnter email to access
#!/usr/bin/env bash
# =============================================================================
# Ubuntu 24.04 LTS VM Bootstrap Script
#
# First-time setup (bare git repo):
#   gh auth login
#   git clone --bare https://github.com/starmorph/dotfiles $HOME/.dotfiles
#   git --git-dir=$HOME/.dotfiles --work-tree=$HOME checkout main -f
#   ~/.bootstrap.sh
#
# Re-run (already cloned):
#   ~/.bootstrap.sh
# =============================================================================
set -euo pipefail

LOG_PREFIX="\033[1;34m==>\033[0m"

log() { echo -e "${LOG_PREFIX} $1"; }
warn() { echo -e "\033[1;33m==> WARNING:\033[0m $1"; }
err() { echo -e "\033[1;31m==> ERROR:\033[0m $1" >&2; }

# ---------------------------------------------------------------------------
# 0. Pre-flight checks
# ---------------------------------------------------------------------------
if [[ "$(uname)" != "Linux" ]]; then
  err "This script is designed for Linux (Ubuntu). Detected: $(uname)"
  exit 1
fi

if ! command -v apt-get &>/dev/null; then
  err "apt-get not found. This script requires a Debian/Ubuntu system."
  exit 1
fi

log "Starting bootstrap..."
log "Detected: $(lsb_release -ds 2>/dev/null || cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2)"

# ---------------------------------------------------------------------------
# 1. System update
# ---------------------------------------------------------------------------
log "Updating system packages..."
# Suppress needrestart interactive prompts during apt operations
export NEEDRESTART_MODE=a
export DEBIAN_FRONTEND=noninteractive
sudo apt-get update -y
sudo apt-get upgrade -y

# ---------------------------------------------------------------------------
# 2. Core packages
# ---------------------------------------------------------------------------
log "Installing core packages..."

APT_PACKAGES=(
  # Shell
  zsh

  # Dev essentials
  git
  curl
  wget
  unzip
  build-essential
  software-properties-common
  gpg               # needed for adding external repos

  # Terminal tools
  tmux
  ripgrep
  fd-find            # binary is fdfind, aliased in .zshrc.linux
  fzf
  jq
  htop
  tree
  bat                # binary is batcat on Ubuntu, aliased in .zshrc.linux
  eza                # modern ls replacement (icons + grid)
  stow
  tig
  mc                 # midnight-commander
  xclip              # clipboard support (pbcopy/pbpaste aliases)

  # Media / processing
  ffmpeg
  imagemagick

  # Networking
  nmap
  net-tools          # ifconfig, netstat

  # Security
  fail2ban           # auto-blocks SSH brute force
  ufw                # firewall

  # System monitoring & admin
  ncdu               # interactive disk usage analyzer
  btop               # all-in-one system monitor (CPU, RAM, disk, net)
  lsof
  sysstat            # sar, iostat, mpstat
  iotop              # I/O monitor
  iftop              # network bandwidth per-connection
  nethogs            # network bandwidth per-process

  # Log viewing
  lnav               # TUI log viewer with SQL queries
  multitail          # tail multiple logs in split panes

  # File navigation
  ranger             # vim-style terminal file manager

  # Python
  python3
  python3-pip
  python3-venv
)

sudo apt-get install -y "${APT_PACKAGES[@]}"

# Neovim (PPA — apt version is too old for LazyVim)
if ! nvim --version 2>/dev/null | head -1 | grep -qE 'v0\.(9|[1-9][0-9])'; then
  log "Installing Neovim from PPA (LazyVim requires >= 0.8.0)..."
  if sudo add-apt-repository ppa:neovim-ppa/unstable -y \
    && sudo apt-get update -y \
    && sudo apt-get install -y neovim; then
    log "Neovim $(nvim --version | head -1) installed"
  else
    warn "Failed to install Neovim from PPA — skipping"
  fi
fi

# delta (git pager) — not in default repos
if ! command -v delta &>/dev/null; then
  log "Installing git-delta..."
  DELTA_VERSION="0.18.2"
  ARCH=$(dpkg --print-architecture)
  DELTA_DEB="/tmp/git-delta_${DELTA_VERSION}_${ARCH}.deb"
  if wget -qO "$DELTA_DEB" "https://github.com/dandavison/delta/releases/download/${DELTA_VERSION}/git-delta_${DELTA_VERSION}_${ARCH}.deb" \
    && (sudo dpkg -i "$DELTA_DEB" || sudo apt-get install -f -y); then
    log "git-delta installed"
  else
    warn "Failed to install git-delta — skipping (git diff will still work)"
  fi
  rm -f "$DELTA_DEB"
fi

# ---------------------------------------------------------------------------
# 3. Ghostty terminfo (so SSHing from Ghostty on macOS works correctly)
# ---------------------------------------------------------------------------
if ! infocmp xterm-ghostty &>/dev/null 2>&1; then
  log "Ghostty terminfo not found — install it from your Mac with:"
  log "  infocmp -x xterm-ghostty | ssh USER@THIS-VM 'tic -x -'"
  log "Or enable auto-install in Ghostty config: shell-integration-features = ssh-terminfo"
fi

# ---------------------------------------------------------------------------
# 4. Zsh + Oh My Zsh + Powerlevel10k
# ---------------------------------------------------------------------------
log "Setting up zsh..."

# Set zsh as default shell
if [[ "$SHELL" != "$(which zsh)" ]]; then
  sudo chsh -s "$(which zsh)" "$USER"
  log "Default shell changed to zsh (takes effect on next login)"
fi

# Oh My Zsh
if [[ ! -d "$HOME/.oh-my-zsh" ]]; then
  log "Installing Oh My Zsh..."
  sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
fi

# Powerlevel10k
if [[ ! -d "$HOME/powerlevel10k" ]]; then
  log "Installing Powerlevel10k..."
  git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k
fi

# ---------------------------------------------------------------------------
# 5. Node.js via fnm + pnpm
# ---------------------------------------------------------------------------
log "Setting up Node.js..."

if ! command -v fnm &>/dev/null; then
  log "Installing fnm..."
  if ! curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell; then
    warn "Failed to install fnm — Node.js setup will be skipped"
  fi
fi

# Ensure fnm is on PATH for this session
export PATH="$HOME/.local/share/fnm:$PATH"
eval "$(fnm env)" 2>/dev/null || true

# Remove system Node if installed via apt (ancient v12) — fnm manages Node instead
if dpkg -l nodejs &>/dev/null 2>&1; then
  warn "Removing system Node.js (apt) — fnm will manage Node versions instead"
  sudo apt-get remove -y nodejs npm 2>/dev/null || true
  sudo apt-get autoremove -y
  hash -r  # clear shell's command cache
fi

if ! fnm ls 2>/dev/null | grep -q "lts-latest\|v[0-9]"; then
  log "Installing Node.js LTS via fnm..."
  fnm install --lts
  fnm default lts-latest
fi

# Re-evaluate after node install
export PATH="$HOME/.local/share/fnm:$PATH"
eval "$(fnm env)" 2>/dev/null || true

if ! command -v pnpm &>/dev/null; then
  log "Installing pnpm..."
  npm install -g pnpm
fi

# ---------------------------------------------------------------------------
# 6. GitHub CLI (gh)
# ---------------------------------------------------------------------------
if ! command -v gh &>/dev/null; then
  log "Installing GitHub CLI..."
  sudo mkdir -p -m 755 /etc/apt/keyrings
  wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null
  sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
  sudo apt-get update -y
  sudo apt-get install -y gh
fi

# ---------------------------------------------------------------------------
# 7. Claude Code
# ---------------------------------------------------------------------------
if ! command -v claude &>/dev/null; then
  log "Installing Claude Code..."
  npm install -g @anthropic-ai/claude-code
fi

# ---------------------------------------------------------------------------
# 8. zoxide (smart cd — not in apt)
# ---------------------------------------------------------------------------
if ! command -v zoxide &>/dev/null; then
  log "Installing zoxide..."
  if curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh; then
    log "zoxide installed"
  else
    warn "Failed to install zoxide — skipping (cd will still work)"
  fi
fi

# ---------------------------------------------------------------------------
# 9. Dotfiles (bare git repo at ~/.dotfiles)
# ---------------------------------------------------------------------------
alias dotfiles='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'

if [[ -d "$HOME/.dotfiles" ]]; then
  log "Dotfiles bare repo found at ~/.dotfiles"

  # Configure bare repo
  dotfiles config --local status.showUntrackedFiles no

  # Checkout files (force — bootstrap already ran clone + checkout above)
  dotfiles checkout main -f 2>/dev/null || true

  # SSH config — strip macOS-only UseKeychain directive
  mkdir -p "$HOME/.ssh"
  chmod 700 "$HOME/.ssh"
  if [[ -f "$HOME/.ssh/config" ]]; then
    sed -i '/UseKeychain/d' "$HOME/.ssh/config"
    chmod 600 "$HOME/.ssh/config"
  fi

  # Use Linux-specific zshrc
  cp "$HOME/.zshrc.linux" "$HOME/.zshrc"
else
  warn "Bare repo not found at ~/.dotfiles — skipping dotfiles checkout"
  warn "Clone it first: git clone --bare https://github.com/starmorph/dotfiles \$HOME/.dotfiles"
fi

# Secrets file
if [[ ! -f "$HOME/.zsh_secrets" ]]; then
  log "Creating empty ~/.zsh_secrets — add your API keys here"
  touch "$HOME/.zsh_secrets"
  chmod 600 "$HOME/.zsh_secrets"
fi

# ---------------------------------------------------------------------------
# 10. Firewall (basic lockdown)
# ---------------------------------------------------------------------------
log "Configuring firewall..."
sudo ufw allow OpenSSH
sudo ufw --force enable
log "Firewall enabled. SSH allowed. Add more rules with: sudo ufw allow <port>/tcp"

# ---------------------------------------------------------------------------
# 11. Fail2ban (SSH brute-force protection)
# ---------------------------------------------------------------------------
log "Enabling fail2ban..."
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# ---------------------------------------------------------------------------
# 12. Generate SSH key if none exists
# ---------------------------------------------------------------------------
if [[ ! -f "$HOME/.ssh/id_ed25519" ]]; then
  log "Generating SSH key..."
  ssh-keygen -t ed25519 -C "$USER@$(hostname)" -f "$HOME/.ssh/id_ed25519" -N ""
  echo ""
  log "Add this public key to GitHub: gh ssh-key add ~/.ssh/id_ed25519.pub"
  cat "$HOME/.ssh/id_ed25519.pub"
  echo ""
fi

# ---------------------------------------------------------------------------
# 13. Timezone
# ---------------------------------------------------------------------------
log "Setting timezone to America/Phoenix..."
sudo timedatectl set-timezone America/Phoenix 2>/dev/null || true

# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
echo ""
log "\033[1;32mBootstrap complete!\033[0m"
echo ""
echo "  What was set up:"
echo "    - zsh + Oh My Zsh + Powerlevel10k"
echo "    - neovim + your LazyVim config"
echo "    - tmux with custom config (prefix: Ctrl-a)"
echo "    - Node.js LTS (via fnm) + pnpm"
echo "    - Claude Code"
echo "    - git-delta, ripgrep, fd, fzf, bat, jq, zoxide"
echo "    - btop, lnav, ranger, iftop, nethogs, multitail"
echo "    - fail2ban + ufw firewall (SSH only)"
echo ""
echo "  Next steps:"
echo "    1. Log out and back in (or run: exec zsh)"
echo "    2. Add API keys to ~/.zsh_secrets"
echo "    3. Run: gh auth login (if not already authenticated)"
echo "    4. Open more ports as needed: sudo ufw allow 3000/tcp"
echo "    5. Use 'dotfiles' alias to manage config: dotfiles status, dotfiles add, etc."
echo ""

How to use

Run chmod +x bootstrap.sh && ./bootstrap.sh on a fresh Ubuntu 24.04 VM. Or pipe directly: curl -fsSL <url> | bash. Safe to re-run anytime.

About

A single-command bootstrap script that turns a fresh Ubuntu 24.04 VM into a fully configured development environment. Installs Zsh + Oh My Zsh + Powerlevel10k, Node.js via fnm, 35+ modern CLI tools (btop, lnav, ranger, zoxide, ripgrep, fd, delta, ncdu), configures ufw firewall + fail2ban, and checks out your dotfiles. Fully idempotent — safe to re-run anytime.

UbuntuDevOpsAutomationFree
Free

Get notified about new configs. No spam.

What’s Included

  • One-command full VM setup
  • 35+ modern CLI tools installed
  • Zsh + Oh My Zsh + Powerlevel10k
  • Node.js via fnm + pnpm
  • ufw firewall + fail2ban security
  • Fully idempotent (safe to re-run)

1 file included