Every new Ubuntu VM on the XCP-ng pool needed the same post-install housekeeping — updates, timezone, NTP, guest tools, a few baseline packages — done by hand, every time. Instead of repeating that checklist again, I put together a bash script that does it in one pass and only touches the system when told to.
https://github.com/tobiastillstam/Ubuntu-vm-bootstrap
Post-install housekeeping for a freshly installed Ubuntu VM. Detects current state and reports it; only changes anything when
--fixis given (always previewable via--dry-run).
Built for XCP-ng guests first, but it also detects and handles KVM/Proxmox, VMware, Hyper-V, and VirtualBox. Interactive with a terminal attached (a yes/no + confirm-the-default wizard), flag-driven and silent otherwise — including a straight curl | bash pipe.
curl -fsSL https://raw.githubusercontent.com/tobiastillstam/Ubuntu-vm-bootstrap/main/ubuntu-vm-bootstrap.sh | sudo bash
Features
- Audit-first — a plain run only reports what it would do. Nothing changes until
--fixis given, and--dry-runpreviews either way. - Core housekeeping — full
apt update/full-upgrade/autoremove/autoclean, timezone + NTP viasystemd-timesyncd, and a set of baseline CLI packages (curl, git, jq, htop, tmux, and the rest). - Hypervisor-aware guest tools — auto-detects the hypervisor via
systemd-detect-virtand installs the matching guest tools, or takes an override with--hypervisor. - Interactive wizard or fully unattended — asks its way through the settings when a terminal is attached; runs silently under
--yes, automation, or a piped install. - Opt-in SSH hardening (
--harden) — key-only SSH (root login and password auth disabled), plus UFW with an OpenSSH allow rule. Refuses to lock out password auth if noauthorized_keysis found, unless forced. - Opt-in swap, unattended-upgrades, and Zabbix Agent2 — each behind its own flag (
--swap,--unattended-upgrades,--zabbix), with the generated Zabbix PSK printed once and never written to a log. - Idempotent — safe to run again on the same VM. Verified live on Ubuntu 26.04 LTS on XCP-ng across audit, dry-run, a full
--fixwith every optional category, and a second pass to confirm nothing changes twice.
