Last active
July 14, 2026 08:54
-
-
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)
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 | |
| # | |
| # 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