Skip to content

Instantly share code, notes, and snippets.

@SubhanRaj
Last active July 14, 2026 08:54
Show Gist options
  • Select an option

  • Save SubhanRaj/f330f93abf1c6bc38ab1362a7ea455e8 to your computer and use it in GitHub Desktop.

Select an option

Save SubhanRaj/f330f93abf1c6bc38ab1362a7ea455e8 to your computer and use it in GitHub Desktop.
One-shot Ubuntu Desktop dev-box provisioning (PHP 8.4, MariaDB, Node 24, VS Code, Warp, Ollama, Claude Code, Cloudflare toolchain, oh-my-zsh+p10k)
#!/usr/bin/env bash
#
# One-shot dev-box provisioning for Ubuntu Desktop 26.04 LTS.
# Target: fresh AIO (i7-13700 / 32GB) used for pdf-markdown-pipeline dev + OCR engine testing.
#
# Run it, answer the few prompts, walk away. Re-running is safe — every step
# checks "is this already installed/done" before doing anything.
#
# Download + run on a fresh machine (copy-paste this whole block):
#
# curl -fsSL https://gist.githubusercontent.com/SubhanRaj/f330f93abf1c6bc38ab1362a7ea455e8/raw/provision-ubuntu-dev.sh -o provision.sh
# chmod +x provision.sh
# ./provision.sh
#
# Usage: chmod +x provision-ubuntu-dev.sh && ./provision-ubuntu-dev.sh
set -uo pipefail
# Deliberately no `-e`: one flaky apt mirror or missing dependency three sections in
# should not kill the whole run. The ERR trap below logs every failure loudly and
# keeps going; check the summary printed at the end, then just re-run the script
# (every step below is idempotent — already-done work is skipped) to retry only
# what failed.
FAILED_STEPS=()
trap 'ec=$?; echo -e "\033[1;33m ! command failed (exit $ec, line $LINENO): ${BASH_COMMAND}\033[0m" >&2; FAILED_STEPS+=("${BASH_COMMAND} (exit $ec, line $LINENO)")' ERR
DB_USER="admin"
NODE_MAJOR="24"
SITES_DIR="$HOME/Sites"
# ── helpers ───────────────────────────────────────────────────────────────
c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_reset='\033[0m'
say() { echo -e "${c_blue}==>${c_reset} $*"; }
ok() { echo -e "${c_green} ✓${c_reset} $*"; }
skip() { echo -e "${c_yellow} ·${c_reset} $* (already present, skipping)"; }
ask_yn() { # ask_yn "question" default(y/n) -> returns 0 for yes
local prompt="$1" default="${2:-y}" reply
[[ "$default" == "y" ]] && prompt="$prompt [Y/n] " || prompt="$prompt [y/N] "
read -rp "$prompt" reply || true
reply="${reply:-$default}"
[[ "$reply" =~ ^[Yy] ]]
}
have() { command -v "$1" &>/dev/null; }
if [[ $EUID -eq 0 ]]; then
echo "Don't run this as root — it uses sudo internally where needed." >&2
exit 1
fi
echo "Provisioning dev box for: git, PHP 8.4, MariaDB, Apache, Composer, Node ${NODE_MAJOR}, pnpm,"
echo "Python, VS Code, Warp, oh-my-zsh+p10k, Ollama, Claude CLI/Code, Cloudflare toolchain,"
echo "Playwright, ~/Sites, and QoL CLI tools."
echo
INSTALL_WARP=y; INSTALL_OLLAMA=y
ask_yn "Install Warp terminal?" y || INSTALL_WARP=n
ask_yn "Install Ollama?" y || INSTALL_OLLAMA=n
echo
read -rp "MariaDB username to create [${DB_USER}]: " db_user_input
DB_USER="${db_user_input:-$DB_USER}"
while true; do
read -rsp "MariaDB password for '${DB_USER}': " DB_PASS; echo
read -rsp "Confirm password: " DB_PASS_CONFIRM; echo
[[ -n "$DB_PASS" && "$DB_PASS" == "$DB_PASS_CONFIRM" ]] && break
echo -e "${c_yellow}Passwords empty or didn't match — try again.${c_reset}"
done
unset DB_PASS_CONFIRM
echo
sudo -v
# keep sudo alive for the duration of the script
( while true; do sudo -n true; sleep 60; done ) &
KEEPALIVE_PID=$!
trap 'kill $KEEPALIVE_PID 2>/dev/null || true' EXIT
# ── base apt setup ────────────────────────────────────────────────────────
say "Updating apt and installing base tooling"
sudo apt update -qq
sudo apt install -y -qq software-properties-common ca-certificates curl wget gnupg lsb-release \
apt-transport-https unzip build-essential git
ok "Base packages present"
# ── PHP 8.4 (packages.sury.org — Ondřej Surý's canonical repo, superseding the ──
# ── ppa:ondrej/php Launchpad PPA; both are built per-Ubuntu-codename, so on a ──
# ── very new release the packages may not be rebuilt for it yet either. Fall ──
# ── back to Ubuntu's own bundled PHP if so — composer.json only needs ^8.3. ──
say "PHP 8.4"
PHP_PKGS_VERSIONED="php8.4 php8.4-cli php8.4-common php8.4-mysql php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl libapache2-mod-php8.4"
PHP_PKGS_GENERIC="php php-cli php-common php-mysql php-mbstring php-xml php-curl php-zip php-bcmath php-gd php-intl libapache2-mod-php"
if ! have php || [[ "$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null)" != "8.4" ]]; then
php_installed=false
if sudo curl -fsSL -o /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg 2>/dev/null \
&& echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php.list >/dev/null \
&& sudo apt update -qq 2>/tmp/php-repo.log \
&& sudo apt install -y -qq $PHP_PKGS_VERSIONED 2>>/tmp/php-repo.log; then
sudo update-alternatives --set php /usr/bin/php8.4 || true
php_installed=true
ok "PHP 8.4 installed via packages.sury.org"
fi
if [[ "$php_installed" == false ]]; then
echo -e "${c_yellow} ! packages.sury.org has no build for this Ubuntu codename yet (normal right after${c_reset}"
echo -e "${c_yellow} a new release ships, before Surý rebuilds for it) — falling back to Ubuntu's own PHP.${c_reset}"
sudo rm -f /etc/apt/sources.list.d/php.list /etc/apt/trusted.gpg.d/php.gpg
sudo apt update -qq
sudo apt install -y -qq $PHP_PKGS_GENERIC
ok "PHP $(php -r 'echo PHP_VERSION;') installed from Ubuntu's default repo (composer.json only requires ^8.3, so this is fine — revisit for 8.4 once sury.org catches up)"
fi
else
skip "PHP 8.4"
fi
php -v | head -1
# ── Composer ───────────────────────────────────────────────────────────────
say "Composer"
if have composer; then
skip "Composer"
else
sudo apt install -y -qq composer
ok "Composer installed"
fi
# ── MariaDB ────────────────────────────────────────────────────────────────
say "MariaDB"
if ! have mariadb && ! have mysql; then
sudo apt install -y -qq mariadb-server
fi
sudo systemctl enable --now mariadb
# Non-interactive equivalent of mysql_secure_installation (remove anonymous
# users / test db) + create the ${DB_USER}/${DB_PASS} account Laravel's .env
# will use, for both localhost and 127.0.0.1 (Laravel connects over TCP).
sudo mysql -u root <<SQL
DELETE FROM mysql.global_priv WHERE User='';
DROP DATABASE IF EXISTS test;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'localhost' WITH GRANT OPTION;
GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'127.0.0.1' WITH GRANT OPTION;
FLUSH PRIVILEGES;
SQL
ok "MariaDB running; user '${DB_USER}'/'${DB_PASS}' granted on localhost + 127.0.0.1"
# ── Apache (documented web server for this project — libapache2-mod-php pulled in above) ──
say "Apache"
sudo apt install -y -qq apache2
sudo a2enmod rewrite >/dev/null
sudo systemctl enable --now apache2
ok "Apache running (project docs: local dev normally uses 'php artisan serve' instead — see DEPLOY.md)"
# ── Node (NodeSource — no package.json in this repo, but useful for tooling/CLIs) ──
say "Node ${NODE_MAJOR}.x"
if have node && [[ "$(node -v)" == v${NODE_MAJOR}.* ]]; then
skip "Node ${NODE_MAJOR}"
else
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | sudo -E bash -
sudo apt install -y -qq nodejs
ok "Node $(node -v) installed"
fi
# ── Python ──────────────────────────────────────────────────────────────────
say "Python"
sudo apt install -y -qq python3 python3-venv python3-pip
ok "$(python3 -V)"
# ── Tesseract + Poppler (OCR test harness dependencies) ────────────────────
say "Tesseract OCR + Poppler"
sudo apt install -y -qq tesseract-ocr tesseract-ocr-hin tesseract-ocr-eng poppler-utils
ok "$(tesseract --version | head -1)"
# ── VS Code ─────────────────────────────────────────────────────────────────
say "VS Code"
if have code; then
skip "VS Code"
else
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/packages.microsoft.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" \
| sudo tee /etc/apt/sources.list.d/vscode.list >/dev/null
sudo apt update -qq
sudo apt install -y -qq code
ok "VS Code installed"
fi
# ── Warp (Linux support is newer/less universal — don't fail the whole script if it's unavailable) ──
if [[ "$INSTALL_WARP" == "y" ]]; then
say "Warp terminal"
if have warp-terminal; then
skip "Warp"
elif curl -fsSL https://releases.warp.dev/linux/keys/warp.asc 2>/dev/null | sudo gpg --dearmor -o /usr/share/keyrings/warpdotdev-archive-keyring.gpg 2>/dev/null; then
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/warpdotdev-archive-keyring.gpg] https://releases.warp.dev/linux/deb stable main" \
| sudo tee /etc/apt/sources.list.d/warpdotdev.list >/dev/null
sudo apt update -qq
if sudo apt install -y -qq warp-terminal; then
ok "Warp installed"
else
echo -e "${c_yellow} ! Warp package install failed — check https://www.warp.dev/download manually${c_reset}"
fi
else
echo -e "${c_yellow} ! Warp repo unreachable — skipping, check https://www.warp.dev/download manually${c_reset}"
fi
fi
# ── ~/Sites directory (mirrors your Mac layout — clone repos here, no permission drama) ──
say "~/Sites directory"
mkdir -p "$SITES_DIR"
chmod 755 "$SITES_DIR"
ok "$SITES_DIR ready"
# ── Ollama ──────────────────────────────────────────────────────────────────
if [[ "$INSTALL_OLLAMA" == "y" ]]; then
say "Ollama"
if have ollama; then
skip "Ollama"
else
curl -fsSL https://ollama.com/install.sh | sh
ok "Ollama installed"
fi
fi
# ── oh-my-zsh + Powerlevel10k (matches your Mac's setup) ────────────────────
say "zsh + oh-my-zsh + Powerlevel10k"
sudo apt install -y -qq zsh
if [[ ! -d "$HOME/.oh-my-zsh" ]]; then
RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
ok "oh-my-zsh installed"
else
skip "oh-my-zsh"
fi
P10K_DIR="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k"
if [[ ! -d "$P10K_DIR" ]]; then
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "$P10K_DIR"
ok "Powerlevel10k cloned"
else
skip "Powerlevel10k"
fi
if [[ ! -f "$HOME/.zshrc.provisioned" ]]; then
cat > "$HOME/.zshrc" <<'ZSHRC'
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME="powerlevel10k/powerlevel10k"
plugins=(git)
zstyle ':omz:update' mode disabled
source $ZSH/oh-my-zsh.sh
export SHELL_SESSIONS_DISABLE=1
# Composer global bin
export PATH="$HOME/.config/composer/vendor/bin:$PATH"
# ~/.local/bin — Claude Code's native installer and pip --user both drop binaries here
export PATH="$HOME/.local/bin:$PATH"
# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh.
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
ZSHRC
touch "$HOME/.zshrc.provisioned"
ok ".zshrc written"
else
skip ".zshrc (already provisioned once — not overwriting your edits)"
fi
sudo chsh -s "$(which zsh)" "$USER"
ok "Default shell set to zsh (takes effect on next login)"
# ── Claude CLI / Claude Code (needs Node) ───────────────────────────────────
say "Claude Code CLI"
if have claude; then
skip "Claude Code"
else
# Native standalone-binary installer (no Node/npm dependency, per Anthropic's own
# docs) — deliberately not `npm install -g`, and deliberately not run under sudo,
# since it installs into this user's home (~/.local/bin), not a system path.
curl -fsSL https://claude.ai/install.sh | bash
ok "Claude Code installed"
fi
# ── Cloudflare toolchain (Wrangler CLI + cloudflared — for the ~/Projects Next.js/CF-Pages work) ──
say "Cloudflare toolchain"
if have wrangler; then
skip "Wrangler"
else
sudo npm install -g wrangler
ok "Wrangler $(wrangler --version 2>/dev/null | head -1) installed"
fi
if have cloudflared; then
skip "cloudflared"
else
# This repo is also built per-codename — same fragility as PHP above on a very
# new Ubuntu release. Try it, and fall back to the plain .deb from GitHub
# releases (arch-only, no codename dependency) if apt can't find a Release file.
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/cloudflared.list >/dev/null
if sudo apt update -qq 2>/tmp/cloudflared-repo.log && sudo apt install -y -qq cloudflared 2>>/tmp/cloudflared-repo.log; then
ok "cloudflared installed via pkg.cloudflare.com"
else
echo -e "${c_yellow} ! cloudflared apt repo has no build for this codename yet — falling back to the GitHub .deb${c_reset}"
sudo rm -f /etc/apt/sources.list.d/cloudflared.list
sudo apt update -qq
tmp_deb="$(mktemp --suffix=.deb)"
curl -fsSL -o "$tmp_deb" https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i "$tmp_deb" || sudo apt install -y -f -qq
rm -f "$tmp_deb"
ok "cloudflared installed via direct .deb download"
fi
fi
# ── pnpm (used by up-excise-spatial-revenue-optimizer's workspace) — via corepack, ships with Node ──
say "pnpm"
if have pnpm; then
skip "pnpm"
else
sudo corepack enable
corepack prepare pnpm@latest --activate
ok "pnpm $(pnpm --version 2>/dev/null) installed"
fi
# ── Playwright OS deps (used by excise-revenue-recovery-portal/frontend's e2e tests) ──
# Headless Chromium/Firefox/WebKit need a pile of shared libs Ubuntu doesn't ship by
# default (libnss3, libatk, libasound2, etc). This installs those + the browser
# binaries into a throwaway location; each project still needs its own
# `npm install` — this just means `npx playwright test` won't immediately fail on
# missing system libs the first time you run it in a freshly cloned repo.
say "Playwright browser + OS dependencies"
if npx --yes playwright install --with-deps chromium >/dev/null 2>&1; then
ok "Playwright Chromium + OS deps installed"
else
echo -e "${c_yellow} ! Playwright install failed — retry manually inside a repo that has @playwright/test installed:${c_reset}"
echo " npx playwright install --with-deps"
fi
# ── QoL CLI tools ────────────────────────────────────────────────────────────
say "Quality-of-life CLI tools"
sudo apt install -y -qq htop tmux fzf ripgrep bat jq tree fd-find zoxide
ok "htop, tmux, fzf, ripgrep, bat, jq, tree, fd-find, zoxide installed"
# Ubuntu ships bat/fd as batcat/fdfind to avoid name clashes — symlink the expected names
mkdir -p "$HOME/.local/bin"
[[ -x "$HOME/.local/bin/bat" ]] || ln -sf "$(command -v batcat)" "$HOME/.local/bin/bat" 2>/dev/null || true
[[ -x "$HOME/.local/bin/fd" ]] || ln -sf "$(command -v fdfind)" "$HOME/.local/bin/fd" 2>/dev/null || true
echo
if [[ ${#FAILED_STEPS[@]} -gt 0 ]]; then
echo -e "${c_yellow}==> ${#FAILED_STEPS[@]} step(s) failed and were skipped:${c_reset}"
for step in "${FAILED_STEPS[@]}"; do
echo -e "${c_yellow} ! ${step}${c_reset}"
done
echo -e "${c_yellow} Re-run this script to retry — everything above is idempotent,${c_reset}"
echo -e "${c_yellow} already-installed steps are skipped automatically.${c_reset}"
echo
fi
say "Done."
cat <<SUMMARY
Next manual steps (things a script shouldn't do for you):
1. Log out/in (or reboot) so the zsh default shell + group changes apply.
2. scp your Powerlevel10k config over so the prompt matches your Mac exactly:
scp you@your-mac:~/.p10k.zsh ~/.p10k.zsh
(skip this and just run \`p10k configure\` instead if you'd rather set it up fresh here)
3. git config --global user.name "..." && git config --global email "..."
4. Generate an SSH key for GitHub if you don't already have one on this box:
ssh-keygen -t ed25519 -C "you@example.com"
5. Clone the repo:
cd ~/Sites && git clone git@github.com:SubhanRaj/pdf-markdown-pipeline.git
cd pdf-markdown-pipeline && composer install
cp .env.example .env && php artisan key:generate
# edit .env: DB_USERNAME=${DB_USER} DB_PASSWORD=<the password you just entered>
php artisan migrate && php artisan db:seed --class=UserSeeder
php artisan storage:link
6. Verify the toolchain (see DEPLOY.md "Verifying a deployment"):
tesseract --list-langs | grep -E "^(hin|eng)\$"
pdftoppm -v
7. Ollama models (if installed) are pulled on demand: ollama pull <model>.
8. \`code .\` inside the repo to open VS Code; sign in to VS Code/Warp/Claude Code
with your accounts — this script can't do interactive OAuth logins for you.
SUMMARY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment