Skip to content

Instantly share code, notes, and snippets.

@Gurpartap
Created August 25, 2025 02:31
Show Gist options
  • Save Gurpartap/96cb7c6f6c4aa9c19bc5074a5dd7aee3 to your computer and use it in GitHub Desktop.
Save Gurpartap/96cb7c6f6c4aa9c19bc5074a5dd7aee3 to your computer and use it in GitHub Desktop.
create an installimage-ready Debian 13 tarball
#!/bin/bash
###############################################################################
# build-trixie.sh – create an installimage-ready Debian 13 tarball
###############################################################################
# Strict and predictable shell behavior
set -euo pipefail
# Found AUTOSETUP file '/autosetup'
# Running unattended installimage installation ...
#
# DRIVE1 /dev/nvme0n1
# DRIVE2 /dev/nvme1n1
# SWRAID 1
# SWRAIDLEVEL 0
# HOSTNAME trixie
# IPV4_ONLY no
# USE_KERNEL_MODE_SETTING yes
# PART /boot/efi esp 256M
# PART /boot ext3 1024M
# PART swap swap 32G
# PART / ext4 320G
# PART /srv ext4 all
# IMAGE /root/Debian-1300-trixie-amd64-base.tar.gz
#
#
# ALL DATA ON THE GIVEN DISKS WILL BE DESTROYED!
#
# DO YOU REALLY WANT TO CONTINUE? [y|N] y
#
#
# WARNING:
# Starting installation in 20 seconds ...
# Press X to continue immediately ...
# Installation will DELETE ALL DATA ON DISK(s)!
# Press CTRL-C to abort now!
# => .............
# ───── tweak here if you like ───────────────────────────────────────────────
export ROOT=/root/trixie-rootfs
export IMAGE=/root/Debian-1300-trixie-amd64-base.tar.gz
export HOSTNAME=trixie
# ────────────────────────────────────────────────────────────────────────────
exec unshare --mount --pid --fork -- bash -c "$(cat <<'NSSCRIPT'
set -eo pipefail
mount --make-rprivate / # isolate our mounts
# Bootstrap Debian 13
# Ensure target root is empty to avoid reusing stale trees
if [ -d "$ROOT" ] && [ -n "$(ls -A "$ROOT" 2>/dev/null)" ]; then
if [ "${CLEAN_ROOT:-0}" = "1" ]; then
rm -rf "$ROOT"
else
echo "ERROR: ROOT directory '$ROOT' is not empty. Set CLEAN_ROOT=1 to remove it automatically, or choose a new ROOT path." >&2
exit 1
fi
fi
mkdir -p "$ROOT"
debootstrap --arch=amd64 --components=main,contrib,non-free,non-free-firmware trixie "$ROOT" http://deb.debian.org/debian
# Mount virtual filesystems (devpts fixes posix_openpt)
mkdir -p "$ROOT"{/dev/pts,/run,/tmp}
mount --bind /dev "$ROOT/dev"
mount -t devpts devpts "$ROOT/dev/pts"
mount -t proc none "$ROOT/proc"
mount -t sysfs none "$ROOT/sys"
mount -t tmpfs tmpfs "$ROOT/run"
mount -t tmpfs tmpfs "$ROOT/tmp"
chmod 755 "$ROOT/run"
chmod 1777 "$ROOT/tmp"
# Ensure DNS works inside chroot for apt operations
install -D -m 644 /etc/resolv.conf "$ROOT/etc/resolv.conf"
# Configure inside chroot
chroot "$ROOT" env -i \
HOME=/root \
TERM=xterm-256color \
CHROOT_HOSTNAME="${HOSTNAME}" \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
/bin/bash -e <<CHROOT
# Ensure strict mode inside chroot
set -euo pipefail
echo "${CHROOT_HOSTNAME:-localhost}" > /etc/hostname
# Configure upstream-only apt sources BEFORE any installs (parity structure)
cat > /etc/apt/sources.list << 'EOF'
deb http://deb.debian.org/debian trixie main contrib non-free non-free-firmware
# deb-src http://deb.debian.org/debian trixie main contrib non-free non-free-firmware
deb http://deb.debian.org/debian trixie-updates main contrib non-free non-free-firmware
# deb-src http://deb.debian.org/debian trixie-updates main contrib non-free non-free-firmware
# deb http://deb.debian.org/debian trixie-backports main contrib non-free non-free-firmware
# deb-src http://deb.debian.org/debian trixie-backports main contrib non-free non-free-firmware
deb http://security.debian.org/debian-security trixie-security main contrib non-free non-free-firmware
# deb-src http://security.debian.org/debian-security trixie-security main contrib non-free non-free-firmware
EOF
export DEBIAN_FRONTEND=noninteractive
apt-get update
# zstd first – avoids gzip fallback
apt-get install --no-install-recommends -y zstd
# locales
apt-get install --no-install-recommends -y locales
echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen
locale-gen
update-locale LANG=en_US.UTF-8
# pre-configure keyboard to avoid interactive prompts
echo 'keyboard-configuration keyboard-configuration/layout select English (US)' | debconf-set-selections
echo 'keyboard-configuration keyboard-configuration/variant select English (US)' | debconf-set-selections
echo 'keyboard-configuration keyboard-configuration/model select Generic 105-key PC' | debconf-set-selections
# essential packages (matching Hetzner's standard + trixie modern defaults)
apt-get install --no-install-recommends -y \
linux-image-amd64 \
initramfs-tools \
initramfs-tools-bin \
initramfs-tools-core \
systemd-sysv \
grub-efi-amd64 \
grub-efi-amd64-bin \
grub-efi-amd64-signed \
grub-pc-bin \
shim-signed \
mdadm \
openssh-server \
dbus \
ifupdown \
net-tools \
iproute2 \
nftables \
unattended-upgrades \
ca-certificates \
gnupg \
curl \
wget \
sudo \
systemd-timesyncd \
isc-dhcp-client \
firmware-bnx2x \
firmware-misc-nonfree \
firmware-realtek \
intel-microcode \
amd64-microcode \
acl \
apt-utils \
at \
bash-completion \
bind9-dnsutils \
bind9-host \
bsdextrautils \
btrfs-progs \
busybox \
bzip2 \
console-setup \
console-setup-linux \
cryptsetup \
cryptsetup-bin \
cryptsetup-initramfs \
dosfstools \
efibootmgr \
ethtool \
file \
gdisk \
htop \
iptables \
keyboard-configuration \
lsb-release \
lsof \
lvm2 \
mailcap \
man-db \
manpages \
media-types \
mtr-tiny \
netcat-traditional \
openssl \
os-prober \
patch \
pci.ids \
pciutils \
perl \
python3 \
python3-apt \
python3-debian \
rsync \
traceroute \
util-linux-extra \
vim \
xfsprogs \
xz-utils
# Strict verification of critical packages (fail fast)
pkgs="linux-image-amd64 initramfs-tools initramfs-tools-bin grub-efi-amd64 grub-efi-amd64-bin grub-efi-amd64-signed shim-signed grub-pc-bin mdadm ifupdown openssh-server"
for p in $pkgs; do
if ! dpkg -s "$p" >/dev/null 2>&1; then
echo "ERROR: required package '$p' not installed" >&2
exit 1
fi
done
# Protect critical packages from unintended removal during cleanup/triggers
apt-mark manual linux-image-amd64 initramfs-tools initramfs-tools-core || true
# Ensure SSH and networking come up on first boot
systemctl enable ssh || true
systemctl enable networking || true
# installimage work-arounds and parity with Bookworm baseline
# Align kernel image handling with baseline
cat > /etc/kernel-img.conf << 'EOF'
# Kernel image management overrides
# See kernel-img.conf(5) for details
do_symlinks = yes
do_bootloader = no
do_initrd = yes
link_in_boot = no
EOF
# Ensure EFI mount exists for systems that need it (installimage will handle fstab)
mkdir -p /boot/efi
: > /etc/mdadm/mdadm.conf # placeholder
# (moved earlier to ensure all installs use these sources)
# create standard interfaces file (matching Hetzner's official images)
cat > /etc/network/interfaces << 'EOF'
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5)
# Include files from /etc/network/interfaces.d:
source /etc/network/interfaces.d/*
# The loopback network interface
auto lo
iface lo inet loopback
EOF
# Bring packages current to latest point updates
apt-get update
apt-get -y upgrade
# final image hygiene and clarity
# - set GRUB defaults to match baseline
cat > /etc/default/grub << 'EOF'
# If you change this file, run 'update-grub' afterwards to update
# /boot/grub/grub.cfg.
# For full documentation of the options in this file, see:
# info -f grub -n 'Simple configuration'
GRUB_DEFAULT=0
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="quiet"
GRUB_CMDLINE_LINUX="consoleblank=0 systemd.show_status=true"
# Uncomment to disable graphical terminal
GRUB_TERMINAL=console
EOF
# - ensure initramfs uses zstd compression (baseline)
test -d /etc/initramfs-tools || { echo "ERROR: /etc/initramfs-tools missing (initramfs-tools not properly installed)" >&2; exit 1; }
if grep -qE '^\s*COMPRESS=' /etc/initramfs-tools/initramfs.conf 2>/dev/null; then
sed -i 's/^\s*COMPRESS=.*/COMPRESS=zstd/' /etc/initramfs-tools/initramfs.conf
else
printf '\nCOMPRESS=zstd\n' >> /etc/initramfs-tools/initramfs.conf
fi
# - mdadm baseline config
cat > /etc/mdadm/mdadm.conf << 'EOF'
# mdadm.conf
# Please refer to mdadm.conf(5) for information about this file.
HOMEHOST <system>
MAILADDR root
EOF
# - enable persistent journal directory (journald uses it automatically)
mkdir -p /var/log/journal
# Generate SSH host keys at first boot if missing (idempotent)
cat > /etc/systemd/system/ssh-hostkeys.service << 'EOF'
[Unit]
Description=Generate SSH host keys at first boot if missing
Before=ssh.service
[Service]
Type=oneshot
ExecStart=/usr/bin/ssh-keygen -A
[Install]
WantedBy=multi-user.target
EOF
systemctl enable ssh-hostkeys.service || true
# - rebuild initramfs for all installed kernels and verify artifacts exist
command -v update-initramfs >/dev/null 2>&1 || { echo "ERROR: update-initramfs not found" >&2; exit 1; }
update-initramfs -u -k all
ls /boot/vmlinuz-* >/dev/null 2>&1 || { echo "ERROR: kernel image missing in /boot (vmlinuz-*)" >&2; exit 1; }
ls /boot/initrd.img-* >/dev/null 2>&1 || { echo "ERROR: initramfs image missing in /boot (initrd.img-*)" >&2; exit 1; }
# - empty resolv.conf to avoid leaking build-host resolvers; installimage will write it
: > /etc/resolv.conf
# - ensure no pre-generated SSH host keys are shipped; systemd/sshd-keygen will create on first boot
rm -f /etc/ssh/ssh_host_* || true
# - ensure a fresh machine-id is created on first boot
truncate -s 0 /etc/machine-id || true
ln -sf /etc/machine-id /var/lib/dbus/machine-id || true
# - remove random-seed to avoid entropy reuse
rm -f /var/lib/systemd/random-seed || true
# Note: Avoid purging initramfs/dracut/dhcpcd here; removing them can cascade
# and cause the kernel/initramfs to be removed by apt. Keep image neutral.
# - trim logs and temp files to keep image small and pristine
find /var/log -type f -exec truncate -s 0 {} + || true
rm -rf /var/tmp/* || true
apt-get clean
rm -rf /var/lib/apt/lists/*
# Final sanity: verify boot artifacts still present
ls /boot/vmlinuz-* >/dev/null 2>&1 || { echo "ERROR: kernel image missing in /boot (vmlinuz-*)" >&2; exit 1; }
ls /boot/initrd.img-* >/dev/null 2>&1 || { echo "ERROR: initramfs image missing in /boot (initrd.img-*)" >&2; exit 1; }
CHROOT
# Unmount (namespace-local)
umount -l "$ROOT"/{proc,sys,dev/pts,dev,run,tmp}
# Create the tarball
tar --numeric-owner -c -z -p \
-f "$IMAGE" \
-C "$ROOT" \
--exclude=dev \
--exclude=proc \
--exclude=sys \
--exclude=run/* \
--exclude=tmp/* \
.
echo "==== Tarball created at: $IMAGE ===="
NSSCRIPT
)"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment