From 69ff5f955bafd35d363fc4cc20560ef716522629 Mon Sep 17 00:00:00 2001
From: Andrey Prokopenko <9478806+terem42@users.noreply.github.com>
Date: Sun, 26 Oct 2025 17:25:17 +0100
Subject: [PATCH] new OS versions added (#87)
---
README.md | 14 +-
hetzner-debian13-zfs-setup.sh | 951 +++++++++++++++++++++++++++++++++
hetzner-ubuntu24-zfs-setup.sh | 973 ++++++++++++++++++++++++++++++++++
3 files changed, 1937 insertions(+), 1 deletion(-)
create mode 100644 hetzner-debian13-zfs-setup.sh
create mode 100644 hetzner-ubuntu24-zfs-setup.sh
diff --git a/README.md b/README.md
index 5b5b62c..e7ea077 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://github.com/terem42/zfs-hetzner-vm/actions/workflows/shellcheck.yml)
-Scripts to install Debian 10, 11, 12 or Ubuntu 18 LTS, 20 LTS, 22 LTS with ZFS root on Hetzner root servers (virtual and dedicated).
+Scripts to install Debian 10, 11, 12, 13 or Ubuntu 18 LTS, 20 LTS, 22 LTS, 24 LTS with ZFS root on Hetzner root servers (virtual and dedicated).
__WARNING:__ all data on the disk will be destroyed.
## How to use:
@@ -30,6 +30,12 @@ Debian 12 minimal setup with SSH server
wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-debian12-zfs-setup.sh | bash -
````
+Debian 13 minimal setup with SSH server
+
+````bash
+wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-debian13-zfs-setup.sh | bash -
+````
+
Ubuntu 18.04 LTS minimal setup with SSH server
````bash
@@ -48,6 +54,12 @@ Ubuntu 22 LTS minimal setup with SSH server
wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-ubuntu22-zfs-setup.sh | bash -
````
+Ubuntu 24 LTS minimal setup with SSH server
+
+````bash
+wget -qO- https://raw.githubusercontent.com/terem42/zfs-hetzner-vm/master/hetzner-ubuntu24-zfs-setup.sh | bash -
+````
+
Answer script questions about desired hostname and ZFS ARC cache size.
To cope with network failures its higly recommended to run the commands above inside screen console, type `man screen` for more info.
diff --git a/hetzner-debian13-zfs-setup.sh b/hetzner-debian13-zfs-setup.sh
new file mode 100644
index 0000000..876ae71
--- /dev/null
+++ b/hetzner-debian13-zfs-setup.sh
@@ -0,0 +1,951 @@
+#!/bin/bash
+
+: <<'end_header_info'
+(c) Andrey Prokopenko job@terem.fr
+fully automatic script to install Debian 13 with ZFS root on Hetzner VPS
+WARNING: all data on the disk will be destroyed
+How to use: add SSH key to the rescue console, then press "mount rescue and power cycle" button
+Next, connect via SSH to console, and run the script
+Answer script questions about desired hostname and ZFS ARC cache size
+To cope with network failures its higly recommended to run the script inside screen console
+screen -dmS zfs
+screen -r zfs
+To detach from screen console, hit Ctrl-d then a
+end_header_info
+
+set -euo pipefail
+
+# ---- Configuration ----
+SYSTEM_HOSTNAME=""
+ROOT_PASSWORD=""
+ZFS_POOL=""
+DEBIAN_CODENAME="trixie" # Debian 13
+TARGET="/mnt/debian"
+
+ZBM_BIOS_URL="https://github.com/zbm-dev/zfsbootmenu/releases/download/v3.0.1/zfsbootmenu-release-x86_64-v3.0.1-linux6.1.tar.gz"
+ZBM_EFI_URL="https://github.com/zbm-dev/zfsbootmenu/releases/download/v3.0.1/zfsbootmenu-release-x86_64-v3.0.1-linux6.1.EFI"
+
+MAIN_BOOT="/main_boot"
+
+# Hetzner mirrors for Debian
+MIRROR_SITE="https://mirror.hetzner.com"
+MIRROR_MAIN="deb ${MIRROR_SITE}/debian/packages ${DEBIAN_CODENAME} main contrib non-free non-free-firmware"
+MIRROR_UPDATES="deb ${MIRROR_SITE}/debian/packages ${DEBIAN_CODENAME}-updates main contrib non-free non-free-firmware"
+MIRROR_SECURITY="deb ${MIRROR_SITE}/debian/security ${DEBIAN_CODENAME}-security main contrib non-free non-free-firmware"
+
+# Global variables
+INSTALL_DISK=""
+EFI_MODE=false
+BOOT_LABEL=""
+BOOT_TYPE=""
+BOOT_PART=""
+ZFS_PART=""
+
+# ---- User Input Functions ----
+function setup_whiptail_colors {
+ # Green text on black background - classic terminal theme
+ export NEWT_COLORS='
+ root=green,black
+ window=green,black
+ shadow=green,black
+ border=green,black
+ title=green,black
+ textbox=green,black
+ button=black,green
+ listbox=green,black
+ actlistbox=black,green
+ actsellistbox=black,green
+ checkbox=green,black
+ actcheckbox=black,green
+ entry=green,black
+ label=green,black
+ '
+}
+
+function check_whiptail {
+ if ! command -v whiptail &> /dev/null; then
+ echo "Installing whiptail..."
+ apt update
+ apt install -y whiptail
+ fi
+ setup_whiptail_colors
+}
+
+function get_hostname {
+ while true; do
+ SYSTEM_HOSTNAME=$(whiptail \
+ --title " System Hostname " \
+ --inputbox "\nEnter the hostname for the new system:" \
+ 10 60 "zfs-debian" \
+ 3>&1 1>&2 2>&3)
+
+ local exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ echo "Installation cancelled by user."
+ exit 1
+ fi
+
+ # Validate hostname
+ if [[ "$SYSTEM_HOSTNAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$ ]] && [[ ${#SYSTEM_HOSTNAME} -le 63 ]]; then
+ break
+ else
+ whiptail \
+ --title " Invalid Hostname " \
+ --msgbox "Invalid hostname. Please use only letters, numbers, and hyphens. Must start and end with alphanumeric character. Maximum 63 characters." \
+ 12 60
+ fi
+ done
+}
+
+function get_zfs_pool_name {
+ while true; do
+ ZFS_POOL=$(whiptail \
+ --title " ZFS Pool Name " \
+ --inputbox "\nEnter the name for the ZFS pool:" \
+ 10 60 "rpool" \
+ 3>&1 1>&2 2>&3)
+
+ local exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ echo "Installation cancelled by user."
+ exit 1
+ fi
+
+ # Validate ZFS pool name
+ if [[ "$ZFS_POOL" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] && [[ ${#ZFS_POOL} -le 255 ]]; then
+ break
+ else
+ whiptail \
+ --title " Invalid Pool Name " \
+ --msgbox "Invalid ZFS pool name. Must start with a letter and contain only letters, numbers, hyphens, and underscores. Maximum 255 characters." \
+ 12 60
+ fi
+ done
+}
+
+function get_root_password {
+ while true; do
+ # Get first password input
+ local password1
+ local password2
+
+ password1=$(whiptail \
+ --title " Root Password " \
+ --passwordbox "\nEnter root password (input hidden):" \
+ 10 60 \
+ 3>&1 1>&2 2>&3)
+
+ local exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ echo "Installation cancelled by user."
+ exit 1
+ fi
+
+ # Get password confirmation
+ password2=$(whiptail \
+ --title " Confirm Root Password " \
+ --passwordbox "\nConfirm root password (input hidden):" \
+ 10 60 \
+ 3>&1 1>&2 2>&3)
+
+ exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ echo "Installation cancelled by user."
+ exit 1
+ fi
+
+ # Check if passwords match
+ if [ "$password1" = "$password2" ]; then
+ if [ -n "$password1" ]; then
+ ROOT_PASSWORD="$password1"
+ break
+ else
+ whiptail \
+ --title " Empty Password " \
+ --msgbox "Password cannot be empty. Please enter a password." \
+ 10 50
+ fi
+ else
+ whiptail \
+ --title " Password Mismatch " \
+ --msgbox "Passwords do not match. Please try again." \
+ 10 50
+ fi
+ done
+}
+
+function show_summary_and_confirm {
+ local summary="Please review the installation settings:
+
+Hostname: $SYSTEM_HOSTNAME
+ZFS Pool: $ZFS_POOL
+Debian Version: $DEBIAN_CODENAME (13)
+Target: $TARGET
+Boot Mode: $([ "$EFI_MODE" = true ] && echo "EFI" || echo "BIOS")
+Install Disk: $INSTALL_DISK
+
+*** WARNING: This will DESTROY ALL DATA on $INSTALL_DISK! ***
+
+Do you want to continue with the installation?"
+
+ if whiptail \
+ --title " Installation Summary " \
+ --yesno "$summary" \
+ 18 60; then
+ # User confirmed - just continue silently
+ echo "User confirmed installation. Starting now..."
+ else
+ echo "Installation cancelled by user."
+ exit 1
+ fi
+}
+
+function get_user_input {
+ echo "======= Gathering Installation Parameters =========="
+ check_whiptail
+
+ # Show welcome message
+ whiptail \
+ --title " ZFS Debian Installer " \
+ --msgbox "Welcome to the ZFS Debian Installer for Hetzner Cloud.\n\nThis script will install Debian 13 with ZFS root on your server." \
+ 12 60
+
+ # Get user inputs
+ get_hostname
+ get_zfs_pool_name
+ get_root_password
+}
+
+# ---- System Detection Functions ----
+function detect_efi {
+ echo "======= Detecting EFI support =========="
+
+ if [ -d /sys/firmware/efi ]; then
+ echo "✓ EFI firmware detected"
+ EFI_MODE=true
+ BOOT_LABEL="EFI"
+ BOOT_TYPE="ef00"
+ else
+ echo "✓ Legacy BIOS mode detected"
+ EFI_MODE=false
+ BOOT_LABEL="boot"
+ BOOT_TYPE="8300"
+ fi
+}
+
+function find_install_disk {
+ echo "======= Finding install disk =========="
+
+ local candidate_disks=()
+
+ # Use lsblk to find all unmounted, writable disks
+ while IFS= read -r disk; do
+ [[ -n "$disk" ]] && candidate_disks+=("$disk")
+ done < <(lsblk -npo NAME,TYPE,RO,MOUNTPOINT | awk '
+ $2 == "disk" && $3 == "0" && $4 == "" {print $1}
+ ')
+
+ if [[ ${#candidate_disks[@]} -eq 0 ]]; then
+ echo "No suitable installation disks found" >&2
+ echo "Looking for: unmounted, writable disks without partitions in use" >&2
+ exit 1
+ fi
+
+ INSTALL_DISK="${candidate_disks[0]}"
+ echo "Using installation disk: $INSTALL_DISK"
+
+ # Show all available disks for verification
+ echo "All available disks:"
+ lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,RO | grep -v loop
+}
+
+# ---- Rescue System Preparation Functions ----
+function remove_unused_kernels {
+ echo "=========== Removing unused kernels in rescue system =========="
+ for kver in $(find /lib/modules/* -maxdepth 0 -type d \
+ | grep -v "$(uname -r)" \
+ | cut -s -d "/" -f 4); do
+
+ for pkg in "linux-headers-$kver" "linux-image-$kver"; do
+ if dpkg -l "$pkg" 2>/dev/null | grep -q '^ii'; then
+ echo "Purging $pkg ..."
+ apt purge --yes "$pkg"
+ else
+ echo "Package $pkg not installed, skipping."
+ fi
+ done
+ done
+}
+
+function install_zfs_on_rescue_system {
+ echo "======= Installing ZFS on rescue system =========="
+ echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
+ # Enable backports for newer ZFS version
+ echo "deb http://mirror.hetzner.com/debian/packages bookworm-backports main contrib" > /etc/apt/sources.list.d/backports.list
+ apt update
+ apt -t bookworm-backports install -y zfsutils-linux
+}
+
+# ---- Disk Partitioning Functions ----
+function partition_disk {
+ echo "======= Partitioning disk =========="
+ sgdisk -Z "$INSTALL_DISK"
+
+ if [ "$EFI_MODE" = true ]; then
+ echo "Creating EFI partition layout"
+ # EFI System Partition (ESP) - 64MB is plenty for ZFSBootMenu
+ sgdisk -n1:1M:+128M -t1:ef00 -c1:"EFI" "$INSTALL_DISK"
+ # ZFS partition
+ sgdisk -n2:0:0 -t2:bf00 -c2:"zfs" "$INSTALL_DISK"
+ else
+ echo "Creating BIOS partition layout"
+ # /boot partition - 64MB is also sufficient for BIOS ZFSBootMenu
+ sgdisk -n1:1M:+128M -t1:8300 -c1:"boot" "$INSTALL_DISK"
+ # ZFS partition
+ sgdisk -n2:0:0 -t2:bf00 -c2:"zfs" "$INSTALL_DISK"
+ # Set legacy BIOS bootable flag
+ sgdisk -A 1:set:2 "$INSTALL_DISK"
+ fi
+
+ partprobe "$INSTALL_DISK" || true
+ udevadm settle
+
+ # Set partition variables based on mode
+ if [ "$EFI_MODE" = true ]; then
+ BOOT_PART="$(blkid -t PARTLABEL='EFI' -o device)"
+ ZFS_PART="$(blkid -t PARTLABEL='zfs' -o device)"
+ # Format ESP as FAT32
+ mkfs.fat -F 32 -n EFI "$BOOT_PART"
+ else
+ BOOT_PART="$(blkid -t PARTLABEL='boot' -o device)"
+ ZFS_PART="$(blkid -t PARTLABEL='zfs' -o device)"
+ mkfs.ext4 -F -L boot "$BOOT_PART"
+ fi
+}
+
+# ---- ZFS Pool and Dataset Functions ----
+function create_zfs_pool {
+ echo "======= Creating ZFS pool =========="
+ # Clean up any existing ZFS binaries in PATH
+ rm -f "$(which zfs)" 2>/dev/null || true
+ rm -f "$(which zpool)" 2>/dev/null || true
+
+ export PATH=/usr/sbin:$PATH
+ modprobe zfs
+
+ zpool create -f -o ashift=12 \
+ -o cachefile="/etc/zfs/zpool.cache" \
+ -O compression=lz4 \
+ -O acltype=posixacl \
+ -O xattr=sa \
+ -O mountpoint=none \
+ "$ZFS_POOL" "$ZFS_PART"
+
+ zfs create -o mountpoint=none "$ZFS_POOL/ROOT"
+ zfs create -o mountpoint=legacy "$ZFS_POOL/ROOT/debian"
+
+ echo "======= Assigning $ZFS_POOL/ROOT/debian dataset as bootable =========="
+ zpool set bootfs="$ZFS_POOL/ROOT/debian" "$ZFS_POOL"
+ zpool set cachefile="/etc/zfs/zpool.cache" "$ZFS_POOL"
+}
+
+function create_additional_zfs_datasets {
+ echo "======= Creating additional ZFS datasets with TEMPORARY mountpoints =========="
+
+ # Ensure parent datasets are created first
+ zfs create -o mountpoint=none "$ZFS_POOL/ROOT/debian/var"
+ zfs create -o mountpoint=none "$ZFS_POOL/ROOT/debian/var/cache"
+
+ # Create leaf datasets with temporary mountpoints under $TARGET
+ zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/tmp" "$ZFS_POOL/ROOT/debian/tmp"
+ zfs set devices=off "$ZFS_POOL/ROOT/debian/tmp"
+
+ zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/var/tmp" "$ZFS_POOL/ROOT/debian/var/tmp"
+ zfs set devices=off "$ZFS_POOL/ROOT/debian/var/tmp"
+
+ zfs create -o mountpoint="$TARGET/var/log" "$ZFS_POOL/ROOT/debian/var/log"
+ zfs set atime=off "$ZFS_POOL/ROOT/debian/var/log"
+
+ zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/var/cache/apt" "$ZFS_POOL/ROOT/debian/var/cache/apt"
+ zfs set atime=off "$ZFS_POOL/ROOT/debian/var/cache/apt"
+
+ # Create home dataset separately
+ zfs create -o mountpoint="$TARGET/home" "$ZFS_POOL/home"
+
+ # Mount all datasets
+ zfs mount -a
+
+ # Set permissions on the actual ZFS datasets
+ echo "Setting permissions on ZFS datasets..."
+ chmod 1777 "$TARGET/tmp"
+ chmod 1777 "$TARGET/var/tmp"
+ echo "✓ Temp directory permissions set (1777)"
+}
+
+function set_final_mountpoints {
+ echo "======= Setting final mountpoints =========="
+
+ # Leaf datasets - actual system mountpoints
+ zfs set mountpoint=/tmp "$ZFS_POOL/ROOT/debian/tmp"
+ zfs set mountpoint=/var/tmp "$ZFS_POOL/ROOT/debian/var/tmp"
+ zfs set mountpoint=/var/log "$ZFS_POOL/ROOT/debian/var/log"
+ zfs set mountpoint=/var/cache/apt "$ZFS_POOL/ROOT/debian/var/cache/apt"
+
+ # Home dataset - separate from OS
+ zfs set mountpoint=/home "$ZFS_POOL/home"
+ echo ""
+ echo "Detailed dataset listing:"
+ zfs list -o name,mountpoint -r "$ZFS_POOL"
+}
+
+# ---- System Bootstrap Functions ----
+function bootstrap_debian_system {
+ echo "======= Bootstrapping Debian to temporary directory =========="
+
+ # Install debootstrap if not available
+ if ! command -v debootstrap &> /dev/null; then
+ echo "Installing debootstrap..."
+ apt update
+ apt install -y debootstrap
+ fi
+
+ #echo "======= Copying staged system to ZFS datasets =========="
+ # Mount root dataset for copying
+ mkdir -p "$TARGET"
+ mount -t zfs "$ZFS_POOL/ROOT/debian" "$TARGET"
+
+ create_additional_zfs_datasets
+
+ # Bootstrap Debian 13 (Trixie) - include dbus to satisfy systemd-resolved dependency
+ debootstrap \
+ --components=main,contrib,non-free,non-free-firmware \
+ --include=initramfs-tools,dbus,locales,debconf-i18n,apt-utils,keyboard-configuration,console-setup,kbd,zstd,systemd-resolved,systemd-timesyncd \
+ "$DEBIAN_CODENAME" \
+ "$TARGET" \
+ "$MIRROR_SITE/debian/packages"
+
+}
+
+function setup_chroot_environment {
+ echo "======= Mounting virtual filesystems for chroot =========="
+ mount -t proc proc "$TARGET/proc"
+ mount -t sysfs sysfs "$TARGET/sys"
+
+ # Only mount specific tmpfs directories, not the entire /run
+ mkdir -p "$TARGET/run/lock" "$TARGET/run/shm"
+ mount -t tmpfs tmpfs "$TARGET/run/lock"
+ mount -t tmpfs tmpfs "$TARGET/run/shm"
+ mount -t tmpfs tmpfs "$TARGET/tmp"
+
+ mount --bind /dev "$TARGET/dev"
+ mount --bind /dev/pts "$TARGET/dev/pts"
+}
+
+# ---- System Configuration Functions ----
+function configure_basic_system {
+ echo "======= Configuring basic system settings =========="
+ chroot "$TARGET" /bin/bash < /etc/hostname
+
+# Configure timezone (Vienna)
+echo "Europe/Vienna" > /etc/timezone
+ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime
+
+# Generate locales
+cat > /etc/locale.gen <<'LOCALES'
+en_US.UTF-8 UTF-8
+de_AT.UTF-8 UTF-8
+fr_FR.UTF-8 UTF-8
+ru_RU.UTF-8 UTF-8
+LOCALES
+
+locale-gen
+
+# Set default locale
+update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
+
+# Configure keyboard for German and US with Alt+Shift toggle
+cat > /etc/default/keyboard <<'KEYBOARD'
+# KEYBOARD CONFIGURATION FILE
+
+# Consult the keyboard(5) manual page.
+
+XKBMODEL="pc105"
+XKBLAYOUT="de,ru"
+XKBVARIANT=","
+XKBOPTIONS="grp:ctrl_shift_toggle"
+
+BACKSPACE="guess"
+KEYBOARD
+
+# Apply keyboard configuration to console
+setupcon --force
+
+# Update /etc/hosts with the hostname
+echo "127.0.0.1 localhost" > /etc/hosts
+echo "127.0.1.1 $SYSTEM_HOSTNAME" >> /etc/hosts
+echo "::1 localhost ip6-localhost ip6-loopback" >> /etc/hosts
+echo "ff02::1 ip6-allnodes" >> /etc/hosts
+echo "ff02::2 ip6-allrouters" >> /etc/hosts
+
+# Set proper permissions for ZFS datasets
+chmod 1777 /tmp
+chmod 1777 /var/tmp
+EOF
+
+ echo "======= Configuration Summary ======="
+ chroot "$TARGET" /bin/bash <<'EOF'
+echo "Hostname: $(cat /etc/hostname)"
+echo "Timezone: $(cat /etc/timezone)"
+echo "Current time: $(date)"
+echo "Default locale: $(grep LANG /etc/default/locale)"
+echo "Available locales:"
+locale -a | grep -E "(en_US|de_AT|fr_FR|ru_RU)"
+echo "Keyboard layout: $(grep XKBLAYOUT /etc/default/keyboard)"
+EOF
+}
+
+function install_system_packages {
+ echo "======= Installing ZFS and essential packages in chroot =========="
+ chroot "$TARGET" /bin/bash <<'EOF'
+set -euo pipefail
+
+# Configure sources.list for Debian 13
+cat > /etc/apt/sources.list <<'SOURCES'
+deb https://mirror.hetzner.com/debian/packages trixie main contrib non-free non-free-firmware
+deb https://mirror.hetzner.com/debian/packages trixie-updates main contrib non-free non-free-firmware
+deb https://mirror.hetzner.com/debian/security trixie-security main contrib non-free non-free-firmware
+deb https://mirror.hetzner.com/debian/packages trixie-backports main contrib non-free non-free-firmware
+SOURCES
+
+# Update package lists
+apt update
+
+# Install kernel
+apt install -y --no-install-recommends linux-image-cloud-amd64 linux-headers-cloud-amd64
+
+# Install essential packages
+apt install -y curl nano htop net-tools ssh \
+ apt-transport-https ca-certificates gnupg dirmngr \
+ firmware-linux-free apparmor
+
+echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
+
+apt install -y -t trixie-backports zfsutils-linux zfs-initramfs zfs-dkms
+
+# Get the actual kernel version installed in the chroot
+KERNEL_VERSION=$(ls /lib/modules/ | head -n1)
+echo "Detected kernel version: $KERNEL_VERSION"
+
+# Verify ZFS module is available in the chroot filesystem
+echo "=== Verifying ZFS module in chroot ==="
+if find "/lib/modules/$KERNEL_VERSION" -name "*zfs*" -type f | grep -q .; then
+ echo "✓ ZFS module files found in /lib/modules/$KERNEL_VERSION/"
+ find "/lib/modules/$KERNEL_VERSION" -name "*zfs*" -type f
+else
+ echo "✗ ZFS module files not found - attempting DKMS rebuild"
+ dkms autoinstall -k "$KERNEL_VERSION" || true
+ depmod -a "$KERNEL_VERSION"
+
+ # Check again after DKMS rebuild
+ if find "/lib/modules/$KERNEL_VERSION" -name "*zfs*" -type f | grep -q .; then
+ echo "✓ ZFS module files found after DKMS rebuild"
+ else
+ echo "✗ ZFS module files still not found - this may cause boot issues"
+ fi
+fi
+
+# Ensure ZFS module is included in initramfs
+echo "zfs" >> /etc/initramfs-tools/modules
+
+# Generate initramfs with ZFS support
+update-initramfs -u -k all
+
+# Verify kernel installation
+echo "Installed kernel packages:"
+dpkg -l | grep linux-image
+echo "Kernel version:"
+ls /lib/modules/
+echo "Kernel files in ZFS dataset:"
+ls -la /boot/vmlinuz* /boot/initrd.img* 2>/dev/null || echo "No kernel files found"
+EOF
+}
+
+function verify_initramfs {
+ echo "======= Verifying initramfs contents =========="
+ chroot "$TARGET" /bin/bash <<'EOF'
+set -euo pipefail
+
+echo "=== Checking initramfs for ZFS components ==="
+for initrd in /boot/initrd.img-*; do
+ if [ -f "$initrd" ]; then
+ echo "Checking: $initrd"
+ lsinitramfs "$initrd" | grep -E "(zfs|pool|dataset|spl)" | head -10 || echo "No ZFS components found (this might be normal for first check)"
+ echo "---"
+ fi
+done
+
+echo "=== Checking ZFS module files on disk ==="
+KERNEL_VERSION=$(ls /lib/modules/ | head -n1)
+find "/lib/modules/$KERNEL_VERSION" -name "*zfs*" -type f
+
+echo "=== Testing ZFS commands ==="
+which zpool && zpool --version || echo "zpool not found"
+which zfs && zfs --version || echo "zfs not found"
+
+echo "=== Checking DKMS status ==="
+dkms status || echo "DKMS not available"
+
+echo "=== Checking if ZFS tools are properly installed ==="
+dpkg -l | grep -E "(zfs|spl)"
+
+EOF
+}
+
+function configure_ssh {
+ echo "======= Setting up OpenSSH =========="
+ mkdir -p "$TARGET/root/.ssh/"
+ cp /root/.ssh/authorized_keys "$TARGET/root/.ssh/authorized_keys"
+ sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' "$TARGET/etc/ssh/sshd_config"
+ sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' "$TARGET/etc/ssh/sshd_config"
+
+ chroot "$TARGET" /bin/bash <<'EOF'
+rm /etc/ssh/ssh_host_*
+dpkg-reconfigure openssh-server -f noninteractive
+EOF
+}
+
+function set_root_credentials {
+ echo "======= Setting root password =========="
+ chroot "$TARGET" /bin/bash -c "echo root:$(printf "%q" "$ROOT_PASSWORD") | chpasswd"
+
+ echo "============ Setting up root prompt ============"
+ cat > "$TARGET/root/.bashrc" < /dev/null; then
+ echo "Installing extlinux in rescue system..."
+ apt update
+ apt install -y extlinux
+ fi
+
+ # Install extlinux
+ extlinux --install "$MAIN_BOOT"
+
+ # Create extlinux configuration
+ cat > "$MAIN_BOOT/extlinux.conf" << 'EOF'
+DEFAULT zfsbootmenu
+PROMPT 0
+TIMEOUT 0
+
+LABEL zfsbootmenu
+ LINUX /zfsbootmenu/vmlinuz-bootmenu
+ INITRD /zfsbootmenu/initramfs-bootmenu.img
+ APPEND ro quiet
+EOF
+
+ echo "Generated extlinux.conf:"
+ cat "$MAIN_BOOT/extlinux.conf"
+
+ # Download and install ZFSBootMenu for BIOS
+ local TEMP_ZBM=$(mktemp -d)
+ echo "Downloading ZFSBootMenu for BIOS from: $ZBM_BIOS_URL"
+ curl -L "$ZBM_BIOS_URL" -o "$TEMP_ZBM/zbm.tar.gz"
+ tar -xz -C "$TEMP_ZBM" -f "$TEMP_ZBM/zbm.tar.gz" --strip-components=1
+
+ # Copy ZFSBootMenu to boot partition
+ mkdir -p "$MAIN_BOOT/zfsbootmenu"
+ cp "$TEMP_ZBM"/vmlinuz* "$MAIN_BOOT/zfsbootmenu/"
+ cp "$TEMP_ZBM"/initramfs* "$MAIN_BOOT/zfsbootmenu/"
+
+ # Clean up
+ rm -rf "$TEMP_ZBM"
+
+ echo "ZFSBootMenu files copied to boot partition:"
+ ls -la "$MAIN_BOOT/zfsbootmenu/"
+
+ # Install MBR and set boot flag
+ dd bs=440 conv=notrunc count=1 if="/usr/lib/EXTLINUX/gptmbr.bin" of="$INSTALL_DISK"
+ parted "$INSTALL_DISK" set 1 boot on
+
+ echo "BIOS boot setup complete"
+}
+
+function configure_bootloader {
+ echo "======= Setting up boot based on firmware type =========="
+ if [ "$EFI_MODE" = true ]; then
+ setup_efi_boot
+ else
+ setup_bios_boot
+ fi
+
+ echo "======= Configuring ZFSBootMenu for auto-detection =========="
+ zfs set org.zfsbootmenu:commandline="ro quiet" "$ZFS_POOL/ROOT/debian"
+
+ echo "Boot configuration:"
+ zfs get org.zfsbootmenu:commandline "$ZFS_POOL/ROOT/debian"
+}
+
+# ---- System Services Functions ----
+function configure_system_services {
+ echo "======= Configuring ZFS cachefile in chrooted system =========="
+ mkdir -p "$TARGET/etc/zfs"
+ cp /etc/zfs/zpool.cache "$TARGET/etc/zfs/zpool.cache"
+
+ echo "Cachefile status:"
+ zpool get cachefile "$ZFS_POOL"
+ ls -la "$TARGET/etc/zfs/zpool.cache" && echo "✓ Cachefile ready" || echo "✗ Cachefile failed"
+
+ echo "======= Enabling essential system services =========="
+ chroot "$TARGET" /bin/bash <<'EOF'
+set -euo pipefail
+
+systemctl enable systemd-resolved
+systemctl enable systemd-timesyncd
+systemctl enable systemd-networkd
+
+systemctl enable zfs-import-cache
+systemctl enable zfs-mount
+
+systemctl enable ssh
+systemctl enable apt-daily.timer
+
+echo "Enabled services:"
+systemctl list-unit-files | grep enabled
+EOF
+}
+
+function configure_networking {
+ echo "======= Configuring systemd-networkd for Hetzner Cloud =========="
+
+ # Create systemd-networkd configuration for all ethernet interfaces
+ mkdir -p "$TARGET/etc/systemd/network"
+
+ cat > "$TARGET/etc/systemd/network/10-hetzner.network" <<'EOF'
+[Match]
+Name=ens* enp* eth*
+
+[Network]
+DHCP=yes
+IPv6PrivacyExtensions=yes
+
+[DHCP]
+RouteMetric=100
+UseDNS=yes
+UseDomains=yes
+
+[DHCPv4]
+RouteMetric=100
+UseDNS=yes
+UseDomains=yes
+
+[IPv6AcceptRA]
+RouteMetric=100
+EOF
+
+ echo "systemd-networkd configuration:"
+ cat "$TARGET/etc/systemd/network/10-hetzner.network"
+ echo ""
+}
+
+# ---- Cleanup and Finalization Functions ----
+function unmount_all_datasets_and_partitions {
+ echo "======= Unmounting all datasets =========="
+
+ # First, unmount virtual filesystems that might be using the datasets
+ echo "Unmounting virtual filesystems..."
+ for dir in dev/pts dev tmp run/lock run/shm run sys proc; do
+ if mountpoint -q "$TARGET/$dir"; then
+ echo "Unmounting $TARGET/$dir"
+ umount "$TARGET/$dir" 2>/dev/null || true
+ fi
+ done
+
+ # Give it a moment
+ sleep 2
+
+ # Try to unmount boot partition first
+ if mountpoint -q "$MAIN_BOOT"; then
+ echo "Unmounting boot partition from $MAIN_BOOT"
+ umount "$MAIN_BOOT" 2>/dev/null || true
+ fi
+
+ # Unmount ZFS datasets
+ echo "Unmounting ZFS datasets..."
+ zfs umount -a 2>/dev/null || true
+
+ # Wait for unmounts to complete
+ sleep 2
+
+ # If root dataset is still mounted, try lazy unmount
+ if mountpoint -q "$TARGET"; then
+ echo "Attempting lazy unmount of $TARGET"
+ umount -l "$TARGET" 2>/dev/null || true
+ fi
+
+ # Force unmount any stubborn ZFS datasets
+ if zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep -q "yes"; then
+ echo "Forcing unmount of remaining ZFS datasets..."
+ zfs umount -a -f 2>/dev/null || true
+ fi
+
+ # Final verification and force unmount if still mounted
+ if mountpoint -q "$TARGET"; then
+ echo "WARNING: $TARGET is still mounted! Attempting final cleanup..."
+ # Use fuser to find what's using the mount
+ if command -v fuser &> /dev/null; then
+ fuser -mv "$TARGET" 2>/dev/null || true
+ fi
+ # Force lazy unmount as last resort
+ umount -l "$TARGET" 2>/dev/null || true
+ fi
+
+ # Final ZFS unmount check
+ local mounted_count=0
+ mounted_count=$(zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep -c "yes" || true)
+
+ if [ "$mounted_count" -gt 0 ]; then
+ echo "WARNING: $mounted_count dataset(s) still mounted:"
+ zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep "yes" || true
+ else
+ echo "✓ All ZFS datasets successfully unmounted"
+ fi
+
+ # Verify $TARGET is unmounted
+ if mountpoint -q "$TARGET"; then
+ echo "WARNING: $TARGET is still mounted but continuing..."
+ else
+ echo "✓ $TARGET successfully unmounted"
+ fi
+
+ # Verify $MAIN_BOOT is unmounted
+ if mountpoint -q "$MAIN_BOOT"; then
+ echo "WARNING: $MAIN_BOOT is still mounted!"
+ else
+ echo "✓ $MAIN_BOOT successfully unmounted"
+ fi
+}
+
+function unmount_chroot_environment {
+ echo "======= Unmounting virtual filesystems =========="
+ # Unmount virtual filesystems first
+ for dir in dev/pts dev tmp run sys proc; do
+ if mountpoint -q "$TARGET/$dir"; then
+ echo "Unmounting $TARGET/$dir"
+ umount "$TARGET/$dir" 2>/dev/null || true
+ fi
+ done
+}
+
+function export_zfs_pool {
+ echo "======= Exporting ZFS pool =========="
+ # Export the pool to ensure clean state
+ zpool export "$ZFS_POOL"
+ echo "✓ ZFS pool '$ZFS_POOL' exported successfully"
+}
+
+function show_final_instructions {
+ echo ""
+ echo "=========================================="
+ echo " INSTALLATION COMPLETE! "
+ echo "=========================================="
+ echo ""
+ echo "System Information:"
+ echo " Hostname: $SYSTEM_HOSTNAME"
+ echo " ZFS Pool: $ZFS_POOL"
+ echo " Boot Mode: $([ "$EFI_MODE" = true ] && echo "EFI" || echo "BIOS")"
+ echo " Debian Version: $DEBIAN_CODENAME"
+ echo " Networking: systemd-networkd + systemd-resolved"
+ echo ""
+ echo "=========================================="
+ echo "Rebooting..."
+}
+
+# ---- Main Installation Flow ----
+function main {
+ echo "Starting ZFS Debian 13 installation on Hetzner..."
+
+ # Get user input first
+ get_user_input
+
+ # System detection
+ detect_efi
+ find_install_disk
+
+ # Show summary and get confirmation
+ show_summary_and_confirm
+
+ # Rescue system preparation
+ remove_unused_kernels
+ install_zfs_on_rescue_system
+
+ # Disk partitioning
+ partition_disk
+
+ # ZFS setup
+ create_zfs_pool
+
+ # System installation
+ bootstrap_debian_system
+ setup_chroot_environment
+ configure_basic_system
+ install_system_packages
+
+ verify_initramfs
+ configure_ssh
+ set_root_credentials
+
+ # System configuration
+ configure_system_services
+ configure_networking
+
+ # Bootloader configuration
+ configure_bootloader
+
+ # Finalization
+ unmount_chroot_environment
+
+ unmount_all_datasets_and_partitions
+ set_final_mountpoints
+ export_zfs_pool
+
+ # Completion
+ show_final_instructions
+
+ reboot
+}
+
+# Run main function
+main
\ No newline at end of file
diff --git a/hetzner-ubuntu24-zfs-setup.sh b/hetzner-ubuntu24-zfs-setup.sh
new file mode 100644
index 0000000..f5ce06e
--- /dev/null
+++ b/hetzner-ubuntu24-zfs-setup.sh
@@ -0,0 +1,973 @@
+#!/bin/bash
+
+: <<'end_header_info'
+(c) Andrey Prokopenko job@terem.fr
+fully automatic script to install Ubuntu 24 with ZFS root on Hetzner VPS
+WARNING: all data on the disk will be destroyed
+How to use: add SSH key to the rescue console, then press "mount rescue and power cycle" button
+Next, connect via SSH to console, and run the script
+Answer script questions about desired hostname and ZFS ARC cache size
+To cope with network failures its higly recommended to run the script inside screen console
+screen -dmS zfs
+screen -r zfs
+To detach from screen console, hit Ctrl-d then a
+end_header_info
+
+set -euo pipefail
+
+# ---- Configuration ----
+# These will be set by user input
+SYSTEM_HOSTNAME=""
+ROOT_PASSWORD=""
+ZFS_POOL=""
+UBUNTU_CODENAME="noble" # Ubuntu 24.04
+TARGET="/mnt/ubuntu"
+
+ZBM_BIOS_URL="https://github.com/zbm-dev/zfsbootmenu/releases/download/v3.0.1/zfsbootmenu-release-x86_64-v3.0.1-linux6.1.tar.gz"
+ZBM_EFI_URL="https://github.com/zbm-dev/zfsbootmenu/releases/download/v3.0.1/zfsbootmenu-release-x86_64-v3.0.1-linux6.1.EFI"
+
+MAIN_BOOT="/main_boot"
+
+# Hetzner mirrors
+MIRROR_SITE="https://mirror.hetzner.com"
+MIRROR_MAIN="deb ${MIRROR_SITE}/ubuntu/packages ${UBUNTU_CODENAME} main restricted universe multiverse"
+MIRROR_UPDATES="deb ${MIRROR_SITE}/ubuntu/packages ${UBUNTU_CODENAME}-updates main restricted universe multiverse"
+MIRROR_BACKPORTS="deb ${MIRROR_SITE}/ubuntu/packages ${UBUNTU_CODENAME}-backports main restricted universe multiverse"
+MIRROR_SECURITY="deb ${MIRROR_SITE}/ubuntu/security ${UBUNTU_CODENAME}-security main restricted universe multiverse"
+
+# Global variables
+INSTALL_DISK=""
+EFI_MODE=false
+BOOT_LABEL=""
+BOOT_TYPE=""
+BOOT_PART=""
+ZFS_PART=""
+
+# ---- User Input Functions ----
+function setup_whiptail_colors {
+ # Green text on black background - classic terminal theme
+ export NEWT_COLORS='
+ root=green,black
+ window=green,black
+ shadow=green,black
+ border=green,black
+ title=green,black
+ textbox=green,black
+ button=black,green
+ listbox=green,black
+ actlistbox=black,green
+ actsellistbox=black,green
+ checkbox=green,black
+ actcheckbox=black,green
+ entry=green,black
+ label=green,black
+ '
+}
+
+function check_whiptail {
+ if ! command -v whiptail &> /dev/null; then
+ echo "Installing whiptail..."
+ apt update
+ apt install -y whiptail
+ fi
+ setup_whiptail_colors
+}
+
+
+function get_hostname {
+ while true; do
+ SYSTEM_HOSTNAME=$(whiptail \
+ --title "System Hostname" \
+ --inputbox "Enter the hostname for the new system:" \
+ 10 60 "zfs-ubuntu" \
+ 3>&1 1>&2 2>&3)
+
+ local exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ whiptail --title "Cancelled" --msgbox "Installation cancelled by user." 10 50
+ exit 1
+ fi
+
+ # Validate hostname
+ if [[ "$SYSTEM_HOSTNAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$ ]] && [[ ${#SYSTEM_HOSTNAME} -le 63 ]]; then
+ break
+ else
+ whiptail \
+ --title "Invalid Hostname" \
+ --msgbox "Invalid hostname. Please use only letters, numbers, and hyphens. Must start and end with alphanumeric character. Maximum 63 characters." \
+ 12 60
+ fi
+ done
+}
+
+function get_zfs_pool_name {
+ while true; do
+ ZFS_POOL=$(whiptail \
+ --title "ZFS Pool Name" \
+ --inputbox "Enter the name for the ZFS pool:" \
+ 10 60 "rpool" \
+ 3>&1 1>&2 2>&3)
+
+ local exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ whiptail --title "Cancelled" --msgbox "Installation cancelled by user." 10 50
+ exit 1
+ fi
+
+ # Validate ZFS pool name
+ if [[ "$ZFS_POOL" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] && [[ ${#ZFS_POOL} -le 255 ]]; then
+ break
+ else
+ whiptail \
+ --title "Invalid Pool Name" \
+ --msgbox "Invalid ZFS pool name. Must start with a letter and contain only letters, numbers, hyphens, and underscores. Maximum 255 characters." \
+ 12 60
+ fi
+ done
+}
+
+function get_root_password {
+ while true; do
+ # Get first password input
+ local password1
+ local password2
+
+ password1=$(whiptail \
+ --title "Root Password" \
+ --passwordbox "Enter root password (input hidden):" \
+ 10 60 \
+ 3>&1 1>&2 2>&3)
+
+ local exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ whiptail --title "Cancelled" --msgbox "Installation cancelled by user." 10 50
+ exit 1
+ fi
+
+ # Get password confirmation
+ password2=$(whiptail \
+ --title "Confirm Root Password" \
+ --passwordbox "Confirm root password (input hidden):" \
+ 10 60 \
+ 3>&1 1>&2 2>&3)
+
+ exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ whiptail --title "Cancelled" --msgbox "Installation cancelled by user." 10 50
+ exit 1
+ fi
+
+ # Check if passwords match
+ if [ "$password1" = "$password2" ]; then
+ if [ -n "$password1" ]; then
+ ROOT_PASSWORD="$password1"
+ break
+ else
+ whiptail \
+ --title "Empty Password" \
+ --msgbox "Password cannot be empty. Please enter a password." \
+ 10 50
+ fi
+ else
+ whiptail \
+ --title "Password Mismatch" \
+ --msgbox "Passwords do not match. Please try again." \
+ 10 50
+ fi
+ done
+}
+
+function show_summary_and_confirm {
+ local summary="Please review the installation settings:
+
+Hostname: $SYSTEM_HOSTNAME
+ZFS Pool: $ZFS_POOL
+Ubuntu Version: $UBUNTU_CODENAME (24.04)
+Target: $TARGET
+Boot Mode: $([ "$EFI_MODE" = true ] && echo "EFI" || echo "BIOS")
+Install Disk: $INSTALL_DISK
+
+*** WARNING: This will DESTROY ALL DATA on $INSTALL_DISK! ***
+
+Do you want to continue with the installation?"
+
+ if whiptail \
+ --title " Installation Summary " \
+ --yesno "$summary" \
+ 18 60; then
+ # User confirmed - just continue silently
+ echo "User confirmed installation. Starting now..."
+ else
+ echo "Installation cancelled by user."
+ exit 1
+ fi
+}
+
+function get_user_input {
+ echo "======= Gathering Installation Parameters =========="
+ check_whiptail
+
+ # Show welcome message
+ whiptail \
+ --title "ZFS Ubuntu Installer" \
+ --msgbox "Welcome to the ZFS Ubuntu Installer for Hetzner Cloud.\n\nThis script will install Ubuntu 24.04 with ZFS root on your server." \
+ 12 60
+
+ # Get user inputs
+ get_hostname
+ get_zfs_pool_name
+ get_root_password
+}
+
+# ---- System Detection Functions ----
+function detect_efi {
+ echo "======= Detecting EFI support =========="
+
+ if [ -d /sys/firmware/efi ]; then
+ echo "✓ EFI firmware detected"
+ EFI_MODE=true
+ BOOT_LABEL="EFI"
+ BOOT_TYPE="ef00"
+ else
+ echo "✓ Legacy BIOS mode detected"
+ EFI_MODE=false
+ BOOT_LABEL="boot"
+ BOOT_TYPE="8300"
+ fi
+}
+
+function find_install_disk {
+ echo "======= Finding install disk =========="
+
+ local candidate_disks=()
+
+ # Use lsblk to find all unmounted, writable disks
+ while IFS= read -r disk; do
+ [[ -n "$disk" ]] && candidate_disks+=("$disk")
+ done < <(lsblk -npo NAME,TYPE,RO,MOUNTPOINT | awk '
+ $2 == "disk" && $3 == "0" && $4 == "" {print $1}
+ ')
+
+ if [[ ${#candidate_disks[@]} -eq 0 ]]; then
+ echo "No suitable installation disks found" >&2
+ echo "Looking for: unmounted, writable disks without partitions in use" >&2
+ exit 1
+ fi
+
+ INSTALL_DISK="${candidate_disks[0]}"
+ echo "Using installation disk: $INSTALL_DISK"
+
+ # Show all available disks for verification
+ echo "All available disks:"
+ lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,RO | grep -v loop
+}
+
+# ---- Rescue System Preparation Functions ----
+function remove_unused_kernels {
+ echo "=========== Removing unused kernels in rescue system =========="
+ for kver in $(find /lib/modules/* -maxdepth 0 -type d \
+ | grep -v "$(uname -r)" \
+ | cut -s -d "/" -f 4); do
+
+ for pkg in "linux-headers-$kver" "linux-image-$kver"; do
+ if dpkg -l "$pkg" 2>/dev/null | grep -q '^ii'; then
+ echo "Purging $pkg ..."
+ apt purge --yes "$pkg"
+ else
+ echo "Package $pkg not installed, skipping."
+ fi
+ done
+ done
+}
+
+function install_zfs_on_rescue_system {
+ echo "======= Installing ZFS on rescue system =========="
+ echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
+ # Enable Hetzner bookworm-backports
+ sed -i 's/^# deb http:\/\/mirror.hetzner.com\/debian\/packages bookworm-backports/deb http:\/\/mirror.hetzner.com\/debian\/packages bookworm-backports/' /etc/apt/sources.list
+ apt update
+ apt -t bookworm-backports install -y zfsutils-linux
+}
+
+# ---- Disk Partitioning Functions ----
+function partition_disk {
+ echo "======= Partitioning disk =========="
+ sgdisk -Z "$INSTALL_DISK"
+
+ if [ "$EFI_MODE" = true ]; then
+ echo "Creating EFI partition layout"
+ # EFI System Partition (ESP) - 64MB is plenty for ZFSBootMenu
+ sgdisk -n1:1M:+128M -t1:ef00 -c1:"EFI" "$INSTALL_DISK"
+ # ZFS partition
+ sgdisk -n2:0:0 -t2:bf00 -c2:"zfs" "$INSTALL_DISK"
+ else
+ echo "Creating BIOS partition layout"
+ # /boot partition - 64MB is also sufficient for BIOS ZFSBootMenu
+ sgdisk -n1:1M:+128M -t1:8300 -c1:"boot" "$INSTALL_DISK"
+ # ZFS partition
+ sgdisk -n2:0:0 -t2:bf00 -c2:"zfs" "$INSTALL_DISK"
+ # Set legacy BIOS bootable flag
+ sgdisk -A 1:set:2 "$INSTALL_DISK"
+ fi
+
+ partprobe "$INSTALL_DISK" || true
+ udevadm settle
+
+ # Set partition variables based on mode
+ if [ "$EFI_MODE" = true ]; then
+ BOOT_PART="$(blkid -t PARTLABEL='EFI' -o device)"
+ ZFS_PART="$(blkid -t PARTLABEL='zfs' -o device)"
+ # Format ESP as FAT32
+ mkfs.fat -F 32 -n EFI "$BOOT_PART"
+ else
+ BOOT_PART="$(blkid -t PARTLABEL='boot' -o device)"
+ ZFS_PART="$(blkid -t PARTLABEL='zfs' -o device)"
+ mkfs.ext4 -F -L boot "$BOOT_PART"
+ fi
+}
+
+# ---- ZFS Pool and Dataset Functions ----
+function create_zfs_pool {
+ echo "======= Creating ZFS pool =========="
+ # Clean up any existing ZFS binaries in PATH
+ rm -f "$(which zfs)" 2>/dev/null || true
+ rm -f "$(which zpool)" 2>/dev/null || true
+
+ export PATH=/usr/sbin:$PATH
+ modprobe zfs
+
+ zpool create -f -o ashift=12 \
+ -o cachefile="/etc/zfs/zpool.cache" \
+ -O compression=lz4 \
+ -O acltype=posixacl \
+ -O xattr=sa \
+ -O mountpoint=none \
+ "$ZFS_POOL" "$ZFS_PART"
+
+ zfs create -o mountpoint=none "$ZFS_POOL/ROOT"
+ zfs create -o mountpoint=legacy "$ZFS_POOL/ROOT/ubuntu"
+
+ echo "======= Assigning $ZFS_POOL/ROOT/ubuntu dataset as bootable =========="
+ zpool set bootfs="$ZFS_POOL/ROOT/ubuntu" "$ZFS_POOL"
+ zpool set cachefile="/etc/zfs/zpool.cache" "$ZFS_POOL"
+}
+
+function create_additional_zfs_datasets {
+ echo "======= Creating additional ZFS datasets with TEMPORARY mountpoints =========="
+
+ # Ensure parent datasets are created first
+ zfs create -o mountpoint=none "$ZFS_POOL/ROOT/ubuntu/var"
+ zfs create -o mountpoint=none "$ZFS_POOL/ROOT/ubuntu/var/cache"
+
+ # Create leaf datasets with temporary mountpoints under $TARGET
+ zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/tmp" "$ZFS_POOL/ROOT/ubuntu/tmp"
+ zfs set devices=off "$ZFS_POOL/ROOT/ubuntu/tmp"
+
+ zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/var/tmp" "$ZFS_POOL/ROOT/ubuntu/var/tmp"
+ zfs set devices=off "$ZFS_POOL/ROOT/ubuntu/var/tmp"
+
+ zfs create -o mountpoint="$TARGET/var/log" "$ZFS_POOL/ROOT/ubuntu/var/log"
+ zfs set atime=off "$ZFS_POOL/ROOT/ubuntu/var/log"
+
+ zfs create -o com.sun:auto-snapshot=false -o mountpoint="$TARGET/var/cache/apt" "$ZFS_POOL/ROOT/ubuntu/var/cache/apt"
+ zfs set atime=off "$ZFS_POOL/ROOT/ubuntu/var/cache/apt"
+
+ # Create home dataset separately
+ zfs create -o mountpoint="$TARGET/home" "$ZFS_POOL/home"
+
+ # Mount all datasets
+ zfs mount -a
+
+ # Set permissions on the actual ZFS datasets
+ echo "Setting permissions on ZFS datasets..."
+ chmod 1777 "$TARGET/tmp"
+ chmod 1777 "$TARGET/var/tmp"
+ echo "✓ Temp directory permissions set (1777)"
+}
+
+function set_final_mountpoints {
+ echo "======= Setting final mountpoints =========="
+
+ # Leaf datasets - actual system mountpoints
+ zfs set mountpoint=/tmp "$ZFS_POOL/ROOT/ubuntu/tmp"
+ zfs set mountpoint=/var/tmp "$ZFS_POOL/ROOT/ubuntu/var/tmp"
+ zfs set mountpoint=/var/log "$ZFS_POOL/ROOT/ubuntu/var/log"
+ zfs set mountpoint=/var/cache/apt "$ZFS_POOL/ROOT/ubuntu/var/cache/apt"
+
+ # Home dataset - separate from OS
+ zfs set mountpoint=/home "$ZFS_POOL/home"
+ echo ""
+ echo "Detailed dataset listing:"
+ zfs list -o name,mountpoint -r "$ZFS_POOL"
+}
+
+# ---- System Bootstrap Functions ----
+function bootstrap_ubuntu_system {
+ echo "======= Bootstrapping Ubuntu to temporary directory =========="
+ local TEMP_STAGE=$(mktemp -d)
+ echo "Created temporary staging directory: $TEMP_STAGE"
+
+ # Cleanup function for temp directory
+ cleanup_temp_stage() {
+ if [ -d "$TEMP_STAGE" ]; then
+ echo "Cleaning up temporary staging directory..."
+ rm -rf "$TEMP_STAGE"
+ fi
+ }
+
+ # Add trap to ensure cleanup on script exit
+ trap cleanup_temp_stage EXIT
+
+ # Add Hetzner Ubuntu mirror as trusted
+ echo "deb [trusted=yes] http://mirror.hetzner.com/ubuntu/packages noble main" > /etc/apt/sources.list.d/ubuntu-temp.list
+
+ # Update and download ubuntu-keyring without sandbox warning
+ apt-get update
+ apt-get -o APT::Sandbox::User=root download ubuntu-keyring
+
+ # Verify download was successful
+ if [ ! -f ubuntu-keyring*.deb ]; then
+ echo "ERROR: Failed to download ubuntu-keyring package"
+ exit 1
+ fi
+
+ # Extract the keyring
+ dpkg-deb -x ubuntu-keyring*.deb /tmp/ubuntu-keyring-extract/
+ mkdir -p /usr/share/keyrings
+ cp /tmp/ubuntu-keyring-extract/usr/share/keyrings/ubuntu-archive-keyring.gpg /usr/share/keyrings/
+
+ echo "✓ Downloaded and extracted ubuntu-keyring package"
+
+ # Clean up - remove temporary repository and restore apt state
+ rm -f /etc/apt/sources.list.d/ubuntu-temp.list
+ apt update
+
+ # Clean up downloaded package
+ rm -f ubuntu-keyring*.deb
+
+ apt install -y mmdebstrap
+
+ mmdebstrap --variant=debootstrap \
+ --keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg \
+ --include=systemd-resolved,locales,debconf-i18n,apt-utils,keyboard-configuration,console-setup,kbd,extlinux,initramfs-tools,zstd \
+ "$UBUNTU_CODENAME" "$TEMP_STAGE" \
+ "$MIRROR_MAIN" "$MIRROR_UPDATES" "$MIRROR_BACKPORTS" "$MIRROR_SECURITY"
+
+ echo "======= Copying staged system to ZFS datasets =========="
+ # Mount root dataset for copying
+ mkdir -p "$TARGET"
+ mount -t zfs "$ZFS_POOL/ROOT/ubuntu" "$TARGET"
+
+ create_additional_zfs_datasets
+
+ # Use rsync to copy the entire system (this will populate all datasets)
+ echo "Copying staged system to ZFS datasets..."
+ rsync -aAX "$TEMP_STAGE/" "$TARGET/"
+
+ echo "Staged system copied successfully"
+ echo "Source size: $(du -sh "$TEMP_STAGE")"
+ echo "Target size: $(du -sh "$TARGET")"
+
+ # Clean up temp directory
+ cleanup_temp_stage
+ trap - EXIT
+}
+
+function setup_chroot_environment {
+ echo "======= Mounting virtual filesystems for chroot =========="
+ mount -t proc proc "$TARGET/proc"
+ mount -t sysfs sysfs "$TARGET/sys"
+ mount -t tmpfs tmpfs "$TARGET/run"
+ mount -t tmpfs tmpfs "$TARGET/tmp"
+ mount --bind /dev "$TARGET/dev"
+ mount --bind /dev/pts "$TARGET/dev/pts"
+
+ configure_dns_resolution
+}
+
+function configure_dns_resolution {
+ echo "======= Configuring DNS resolution =========="
+ mkdir -p "$TARGET/run/systemd/resolve"
+
+ if command -v resolvectl >/dev/null 2>&1; then
+ echo "Getting DNS from resolvectl..."
+
+ # First try Global DNS servers
+ local DNS_SERVERS=$(resolvectl dns | awk '
+ /^Global:/ {
+ for(i=2; i<=NF; i++) print $i
+ }
+ ' | head -3)
+
+ # If Global is empty, find first non-empty link
+ if [ -z "$DNS_SERVERS" ]; then
+ echo "No global DNS servers found, searching for first non-empty link..."
+ DNS_SERVERS=$(resolvectl dns | awk '
+ /^Link [0-9]+ / && NF > 3 {
+ for(i=4; i<=NF; i++) print $i # Start from field 4 to skip the interface name
+ exit # Stop after first non-empty link
+ }
+ ')
+ fi
+
+ if [ -n "$DNS_SERVERS" ]; then
+ # Create resolv.conf with the DNS servers
+ echo "$DNS_SERVERS" | while read -r dns; do
+ echo "nameserver $dns"
+ done > "$TARGET/run/systemd/resolve/stub-resolv.conf"
+ echo "Using DNS servers: $(echo "$DNS_SERVERS" | tr '\n' ' ')"
+ else
+ echo "ERROR: No DNS servers found in resolvectl output"
+ echo "resolvectl dns output:"
+ resolvectl dns
+ echo "Cannot continue without DNS configuration"
+ exit 1
+ fi
+ else
+ echo "ERROR: resolvectl command not found"
+ echo "Cannot configure DNS without resolvectl"
+ exit 1
+ fi
+}
+
+# ---- System Configuration Functions ----
+function configure_basic_system {
+ echo "======= Configuring basic system settings =========="
+ chroot "$TARGET" /bin/bash < /etc/hostname
+
+# Configure timezone (Vienna)
+echo "Europe/Vienna" > /etc/timezone
+ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime
+
+# Generate locales
+cat > /etc/locale.gen <<'LOCALES'
+en_US.UTF-8 UTF-8
+de_AT.UTF-8 UTF-8
+fr_FR.UTF-8 UTF-8
+ru_RU.UTF-8 UTF-8
+LOCALES
+
+locale-gen
+
+# Set default locale
+update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
+
+# Configure keyboard for German and US with Alt+Shift toggle
+cat > /etc/default/keyboard <<'KEYBOARD'
+# KEYBOARD CONFIGURATION FILE
+
+# Consult the keyboard(5) manual page.
+
+XKBMODEL="pc105"
+XKBLAYOUT="de,ru"
+XKBVARIANT=","
+XKBOPTIONS="grp:ctrl_shift_toggle"
+
+BACKSPACE="guess"
+KEYBOARD
+
+# Apply keyboard configuration to console
+setupcon --force
+
+# Update /etc/hosts with the hostname
+echo "127.0.0.1 localhost" > /etc/hosts
+echo "127.0.1.1 $SYSTEM_HOSTNAME" >> /etc/hosts
+echo "::1 localhost ip6-localhost ip6-loopback" >> /etc/hosts
+echo "ff02::1 ip6-allnodes" >> /etc/hosts
+echo "ff02::2 ip6-allrouters" >> /etc/hosts
+
+# Set proper permissions for ZFS datasets
+chmod 1777 /tmp
+chmod 1777 /var/tmp
+EOF
+
+ echo "======= Configuration Summary ======="
+ chroot "$TARGET" /bin/bash <<'EOF'
+echo "Hostname: $(cat /etc/hostname)"
+echo "Timezone: $(cat /etc/timezone)"
+echo "Current time: $(date)"
+echo "Default locale: $(grep LANG /etc/default/locale)"
+echo "Available locales:"
+locale -a | grep -E "(en_US|de_AT|fr_FR|ru_RU)"
+echo "Keyboard layout: $(grep XKBLAYOUT /etc/default/keyboard)"
+EOF
+}
+
+function install_system_packages {
+ echo "======= Installing ZFS and essential packages in chroot =========="
+ chroot "$TARGET" /bin/bash <<'EOF'
+set -euo pipefail
+# Update package lists
+apt update
+
+# Install generic kernel (creates files in ZFS dataset /boot)
+apt install -y --no-install-recommends linux-image-generic linux-headers-generic
+
+# Install ZFS utilities and aux packages
+
+echo "zfs-dkms zfs-dkms/note-incompatible-licenses note true" | debconf-set-selections
+
+apt install -y zfs-dkms zfsutils-linux zfs-initramfs software-properties-common bash curl nano htop net-tools ssh
+
+# Ensure ZFS module is included in initramfs
+echo "zfs" >> /etc/initramfs-tools/modules
+
+# Generate initramfs with ZFS support
+update-initramfs -u -k all
+
+# Verify kernel installation
+echo "Installed kernel packages:"
+dpkg -l | grep linux-image
+echo "Kernel version:"
+ls /lib/modules/
+echo "Kernel files in ZFS dataset:"
+ls -la /boot/vmlinuz* /boot/initrd.img* 2>/dev/null || echo "No kernel files found"
+EOF
+}
+
+function configure_ssh {
+ echo "======= Setting up OpenSSH =========="
+ mkdir -p "$TARGET/root/.ssh/"
+ cp /root/.ssh/authorized_keys "$TARGET/root/.ssh/authorized_keys"
+ sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' "$TARGET/etc/ssh/sshd_config"
+ sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' "$TARGET/etc/ssh/sshd_config"
+
+ chroot "$TARGET" /bin/bash <<'EOF'
+rm /etc/ssh/ssh_host_*
+dpkg-reconfigure openssh-server -f noninteractive
+EOF
+}
+
+function set_root_credentials {
+ echo "======= Setting root password =========="
+ chroot "$TARGET" /bin/bash -c "echo root:$(printf "%q" "$ROOT_PASSWORD") | chpasswd"
+
+ echo "============ Setting up root prompt ============"
+ cat > "$TARGET/root/.bashrc" < /dev/null; then
+ echo "Installing extlinux in rescue system..."
+ apt update
+ apt install -y extlinux
+ fi
+
+ # Install extlinux
+ extlinux --install "$MAIN_BOOT"
+
+ # Create extlinux configuration
+ cat > "$MAIN_BOOT/extlinux.conf" << 'EOF'
+DEFAULT zfsbootmenu
+PROMPT 0
+TIMEOUT 0
+
+LABEL zfsbootmenu
+ LINUX /zfsbootmenu/vmlinuz-bootmenu
+ INITRD /zfsbootmenu/initramfs-bootmenu.img
+ APPEND ro quiet
+EOF
+
+ echo "Generated extlinux.conf:"
+ cat "$MAIN_BOOT/extlinux.conf"
+
+ # Download and install ZFSBootMenu for BIOS
+ local TEMP_ZBM=$(mktemp -d)
+ echo "Downloading ZFSBootMenu for BIOS from: $ZBM_BIOS_URL"
+ curl -L "$ZBM_BIOS_URL" -o "$TEMP_ZBM/zbm.tar.gz"
+ tar -xz -C "$TEMP_ZBM" -f "$TEMP_ZBM/zbm.tar.gz" --strip-components=1
+
+ # Copy ZFSBootMenu to boot partition
+ mkdir -p "$MAIN_BOOT/zfsbootmenu"
+ cp "$TEMP_ZBM"/vmlinuz* "$MAIN_BOOT/zfsbootmenu/"
+ cp "$TEMP_ZBM"/initramfs* "$MAIN_BOOT/zfsbootmenu/"
+
+ # Clean up
+ rm -rf "$TEMP_ZBM"
+
+ echo "ZFSBootMenu files copied to boot partition:"
+ ls -la "$MAIN_BOOT/zfsbootmenu/"
+
+ # Install MBR and set boot flag
+ dd bs=440 conv=notrunc count=1 if="$TARGET/usr/lib/EXTLINUX/gptmbr.bin" of="$INSTALL_DISK"
+ parted "$INSTALL_DISK" set 1 boot on
+
+ echo "BIOS boot setup complete"
+}
+
+function configure_bootloader {
+ echo "======= Setting up boot based on firmware type =========="
+ if [ "$EFI_MODE" = true ]; then
+ setup_efi_boot
+ else
+ setup_bios_boot
+ fi
+
+ echo "======= Configuring ZFSBootMenu for auto-detection =========="
+ zfs set org.zfsbootmenu:commandline="ro quiet" "$ZFS_POOL/ROOT/ubuntu"
+
+ echo "Boot configuration:"
+ zfs get org.zfsbootmenu:commandline "$ZFS_POOL/ROOT/ubuntu"
+}
+
+# ---- System Services Functions ----
+function configure_system_services {
+ echo "======= Configuring ZFS cachefile in chrooted system =========="
+ mkdir -p "$TARGET/etc/zfs"
+ cp /etc/zfs/zpool.cache "$TARGET/etc/zfs/zpool.cache"
+
+ echo "Cachefile status:"
+ zpool get cachefile "$ZFS_POOL"
+ ls -la "$TARGET/etc/zfs/zpool.cache" && echo "✓ Cachefile ready" || echo "✗ Cachefile failed"
+
+ echo "======= Enabling essential system services =========="
+ chroot "$TARGET" /bin/bash <<'EOF'
+set -euo pipefail
+
+systemctl enable systemd-resolved
+systemctl enable systemd-timesyncd
+
+systemctl enable zfs-import-cache
+systemctl enable zfs-mount
+
+systemctl enable ssh
+systemctl enable apt-daily.timer
+
+echo "Enabled services:"
+systemctl list-unit-files | grep enabled
+EOF
+}
+
+function configure_networking {
+ echo "======= Configuring Netplan for Hetzner Cloud =========="
+ chroot "$TARGET" /bin/bash <<'EOF'
+set -euo pipefail
+# Create Netplan configuration that matches all non-loopback interfaces
+cat > /etc/netplan/01-hetzner.yaml <<'EOL'
+network:
+ version: 2
+ renderer: networkd
+ ethernets:
+ all-interfaces:
+ match:
+ name: "!lo"
+ dhcp4: true
+ dhcp6: true
+ dhcp4-overrides:
+ use-dns: true
+ use-hostname: true
+ use-domains: true
+ route-metric: 100
+ dhcp6-overrides:
+ use-dns: true
+ use-hostname: true
+ use-domains: true
+ route-metric: 100
+ critical: true
+EOL
+
+# Set proper permissions - Netplan requires strict permissions (600)
+chmod 600 /etc/netplan/01-hetzner.yaml
+chown root:root /etc/netplan/01-hetzner.yaml
+
+# Apply the Netplan configuration
+netplan generate
+echo "Netplan configuration created for all interfaces"
+EOF
+}
+
+# ---- Cleanup and Finalization Functions ----
+function unmount_all_datasets_and_partitions {
+ echo "======= Unmounting all datasets =========="
+
+ # First, unmount all auto-mounted ZFS datasets (tmp, var/tmp, var/log, etc.)
+ echo "Unmounting auto-mounted ZFS datasets..."
+ zfs umount -a 2>/dev/null || true
+
+ # Manually unmount the root legacy dataset from $TARGET
+ if mountpoint -q "$TARGET"; then
+ echo "Unmounting root dataset from $TARGET"
+ umount "$TARGET" 2>/dev/null || true
+ fi
+
+ # Manually unmount boot partition if mounted
+ if mountpoint -q "$MAIN_BOOT"; then
+ echo "Unmounting boot partition from $MAIN_BOOT"
+ umount "$MAIN_BOOT" 2>/dev/null || true
+ fi
+
+ # Wait for unmounts to complete
+ sleep 1
+
+ # Force unmount any stubborn datasets
+ if zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep -q "yes"; then
+ echo "Forcing unmount of remaining ZFS datasets..."
+ zfs umount -a -f 2>/dev/null || true
+ fi
+
+ # Final verification
+ local mounted_count=0
+ mounted_count=$(zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep -c "yes" || true)
+
+ if [ "$mounted_count" -gt 0 ]; then
+ echo "WARNING: $mounted_count dataset(s) still mounted after unmount attempt:"
+ zfs get mounted -r "$ZFS_POOL" 2>/dev/null | grep "yes" || true
+ else
+ echo "✓ All ZFS datasets successfully unmounted"
+ fi
+
+ # Verify $TARGET is unmounted
+ if mountpoint -q "$TARGET"; then
+ echo "WARNING: $TARGET is still mounted!"
+ mount | grep "$TARGET" || true
+ else
+ echo "✓ $TARGET successfully unmounted"
+ fi
+
+ # Verify $MAIN_BOOT is unmounted
+ if mountpoint -q "$MAIN_BOOT"; then
+ echo "WARNING: $MAIN_BOOT is still mounted!"
+ mount | grep "$MAIN_BOOT" || true
+ else
+ echo "✓ $MAIN_BOOT successfully unmounted"
+ fi
+}
+
+function unmount_chroot_environment {
+ echo "======= Unmounting virtual filesystems =========="
+ # Unmount virtual filesystems first
+ for dir in dev/pts dev tmp run sys proc; do
+ if mountpoint -q "$TARGET/$dir"; then
+ echo "Unmounting $TARGET/$dir"
+ umount "$TARGET/$dir" 2>/dev/null || true
+ fi
+ done
+}
+
+function finalize_system_resolved {
+ echo "======= Setting systemd-resolved configuration for final boot =========="
+ # This must be done while $TARGET is still mounted
+ mkdir -p "$TARGET/run/systemd/resolve"
+ cat > "$TARGET/run/systemd/resolve/stub-resolv.conf" << 'EOF'
+nameserver 127.0.0.53
+options edns0 trust-ad
+search .
+EOF
+ echo "✓ systemd-resolved configuration set"
+}
+
+function export_zfs_pool {
+ echo "======= Exporting ZFS pool =========="
+ zpool export "$ZFS_POOL" 2>/dev/null || true
+
+ # Verify everything is unmounted
+ if mountpoint -q "$TARGET"; then
+ echo "WARNING: $TARGET is still mounted!"
+ mount | grep "$TARGET"
+ else
+ echo "✓ All filesystems successfully unmounted"
+ fi
+}
+
+function show_final_instructions {
+ echo ""
+ echo "=========================================="
+ echo " INSTALLATION COMPLETE! "
+ echo "=========================================="
+ echo ""
+ echo "System Information:"
+ echo " Hostname: $SYSTEM_HOSTNAME"
+ echo " ZFS Pool: $ZFS_POOL"
+ echo " Boot Mode: $([ "$EFI_MODE" = true ] && echo "EFI" || echo "BIOS")"
+ echo " Ubuntu Version: $UBUNTU_CODENAME"
+ echo " Networking: systemd-networkd + systemd-resolved"
+ echo ""
+ echo "=========================================="
+ echo "Rebooting..."
+}
+
+# ---- Main Execution Function ----
+function main {
+ echo "Starting ZFS Ubuntu installation on Hetzner Cloud..."
+
+ # Phase 0: User input
+ get_user_input
+
+ # Phase 1: System detection and preparation
+ detect_efi
+ find_install_disk
+
+ # Show summary and get final confirmation
+ show_summary_and_confirm
+
+ remove_unused_kernels
+ install_zfs_on_rescue_system
+
+ # Phase 2: Disk partitioning and ZFS setup
+ partition_disk
+ create_zfs_pool
+
+ # Phase 3: System bootstrap
+ bootstrap_ubuntu_system
+ setup_chroot_environment
+
+ # Phase 4: System configuration
+ configure_basic_system
+ install_system_packages
+ configure_ssh
+ set_root_credentials
+ configure_system_services
+ configure_networking
+
+ # Phase 5: Bootloader setup
+ configure_bootloader
+
+ # Phase 6: Cleanup and finalization
+ unmount_chroot_environment
+ finalize_system_resolved
+ unmount_all_datasets_and_partitions
+
+ # Phase 7: Final mountpoints and export
+ set_final_mountpoints
+
+ export_zfs_pool
+
+ show_final_instructions
+
+ reboot
+}
+
+# Execute main function
+main "$@"
\ No newline at end of file