From b2b4b6dd1953e0d090352a0eefa0b4a5074fdb1b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 02:29:02 +0000 Subject: [PATCH 1/2] Add cloud-init.sh for provider install-script/user-data fields IONOS Cloud Server, DigitalOcean, and Hetzner all offer an "install script"/user-data field that runs as root with no TTY while the image is still provisioning, so bootstrap.sh's interactive tail can't run there. cloud-init.sh clones the repo unattended and drops a one-shot /etc/profile.d hook that launches the normal whiptail setup.sh wizard on the first interactive login, then removes itself. --- README.md | 14 +++++++++++ cloud-init.sh | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100755 cloud-init.sh diff --git a/README.md b/README.md index 1993f87..fe5ae33 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,20 @@ the wizard — the USB can be unplugged once setup starts. sudo bash bootstrap.sh --pat ghp_xxxxxxxxxxxxxxxxxxxx ``` Use a fine-grained read-only PAT scoped to just this repo (Contents: Read). + +**Cloud provider install-script / user-data field (IONOS, DigitalOcean, +Hetzner, ...):** these run as root with no terminal attached while the +image is still being provisioned, so `bootstrap.sh`'s interactive hand-off +doesn't apply yet. Paste `cloud-init.sh` into that field instead: +``` +https://raw.githubusercontent.com/outis1one/ubuntu-post-install/main/cloud-init.sh +``` +It clones the repo in the background during provisioning and installs a +one-shot login hook. The provider boots Ubuntu 24.04, this runs unattended, +and by the time you SSH in the whiptail service menu is already waiting for +you — same experience as `bootstrap.sh`, just already started. Assumes a +root login (the default for all three providers above); see the comments in +`cloud-init.sh` if you've provisioned a separate sudo user instead. The PAT is stripped from the stored remote URL after cloning. ## Usage diff --git a/cloud-init.sh b/cloud-init.sh new file mode 100755 index 0000000..3aba119 --- /dev/null +++ b/cloud-init.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# cloud-init.sh — payload for a cloud provider's "install script" / user-data +# field (IONOS Cloud Server image deploy, DigitalOcean droplet user-data, +# Hetzner Cloud user-data, etc). The provider runs this as root, unattended, +# with no TTY, while the box is still being provisioned — before you have +# ever logged in. +# +# It deliberately does NOT run the interactive wizard itself (there's no +# terminal for whiptail to talk to yet). Instead it does two things: +# +# 1. Clones this repo to /root/ubuntu-post-install (pulls if already there). +# 2. Installs a one-shot /etc/profile.d hook that launches setup.sh — +# the normal whiptail service menu — the first time you actually log +# in over SSH, then deletes itself so it never fires again. +# +# End result: the provider boots Ubuntu 24.04, this runs in the background, +# and by the time you SSH in the checklist menu is sitting there waiting — +# the same experience as running bootstrap.sh by hand, just already started. +# +# Usage: paste this file's raw URL into the provider's install-script / +# user-data field, e.g. +# https://raw.githubusercontent.com/outis1one/ubuntu-post-install/main/cloud-init.sh +# +# Assumes the provider logs you in as root (the default for IONOS Cloud +# Server, DigitalOcean droplets, and Hetzner Cloud server images). If you've +# provisioned a separate non-root sudo user instead, the hook won't reach +# you automatically — SSH in and run: +# sudo bash /root/ubuntu-post-install/setup.sh +set -euo pipefail + +if [ "$(id -u)" -ne 0 ]; then + echo "cloud-init.sh must run as root — that's how provider install-script hooks already run it." >&2 + exit 1 +fi + +REPO_URL="https://github.com/outis1one/ubuntu-post-install.git" +DEST="/root/ubuntu-post-install" +MARKER="/root/.ubuntu-post-install-pending" +HOOK="/etc/profile.d/99-ubuntu-post-install.sh" + +command -v git >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y git; } + +if [ -d "$DEST/.git" ]; then + git -C "$DEST" pull --ff-only || true +else + git clone "$REPO_URL" "$DEST" +fi + +touch "$MARKER" + +# POSIX sh, not bash — /etc/profile.d/*.sh gets sourced by whatever shell +# the login uses, not necessarily bash. +cat > "$HOOK" << 'EOF' +# Installed by cloud-init.sh — launches the ubuntu-post-install wizard on +# the first interactive login, then removes itself so it never fires again. +MARKER="/root/.ubuntu-post-install-pending" +HOOK="/etc/profile.d/99-ubuntu-post-install.sh" +DEST="/root/ubuntu-post-install" + +if [ -f "$MARKER" ] && [ -t 0 ] && [ "$(id -u)" -eq 0 ] && [ -f "$DEST/setup.sh" ]; then + rm -f "$MARKER" "$HOOK" + echo "" + echo "ubuntu-post-install: launching the setup wizard..." + echo "" + bash "$DEST/setup.sh" +fi +EOF +chmod 644 "$HOOK" + +echo "cloud-init.sh: repo cloned to $DEST — the setup wizard will launch on first login." From 5cab7fe9c06de45cc0ce162480c9e1dca3b00932 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 02:33:26 +0000 Subject: [PATCH 2/2] Correct cloud-init.sh usage instructions for real provider UIs IONOS's User Data field takes a Script Type choice (Cloud Config vs Shell Script) and runs the pasted/imported content directly rather than fetching a URL. Update the README to say so, add DEBIAN_FRONTEND=noninteractive for genuine unattended cloud-init execution. --- README.md | 15 +++++++++++---- cloud-init.sh | 1 + 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fe5ae33..0951f62 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,17 @@ Use a fine-grained read-only PAT scoped to just this repo (Contents: Read). **Cloud provider install-script / user-data field (IONOS, DigitalOcean, Hetzner, ...):** these run as root with no terminal attached while the image is still being provisioned, so `bootstrap.sh`'s interactive hand-off -doesn't apply yet. Paste `cloud-init.sh` into that field instead: -``` -https://raw.githubusercontent.com/outis1one/ubuntu-post-install/main/cloud-init.sh -``` +doesn't apply yet. Use `cloud-init.sh` instead — it's a plain cloud-init +user-data shell script (starts with `#!/bin/bash`, no `#cloud-config` YAML). + +IONOS's server-creation screen has a **User Data** box under "Scripts" with +a **Script Type** choice of *Cloud Config* or *Shell Script* — pick +**Shell Script**, then either click **Import from file** and select +`cloud-init.sh`, or paste its contents directly. User-data fields run the +script's own content; they don't fetch a URL, so paste/import the file +itself rather than a link to it. (DigitalOcean/Hetzner's plain "User data" +textbox works the same way — paste the script contents in directly.) + It clones the repo in the background during provisioning and installs a one-shot login hook. The provider boots Ubuntu 24.04, this runs unattended, and by the time you SSH in the whiptail service menu is already waiting for diff --git a/cloud-init.sh b/cloud-init.sh index 3aba119..605f8d6 100755 --- a/cloud-init.sh +++ b/cloud-init.sh @@ -37,6 +37,7 @@ REPO_URL="https://github.com/outis1one/ubuntu-post-install.git" DEST="/root/ubuntu-post-install" MARKER="/root/.ubuntu-post-install-pending" HOOK="/etc/profile.d/99-ubuntu-post-install.sh" +export DEBIAN_FRONTEND=noninteractive command -v git >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y git; }