Skip to content

Instantly share code, notes, and snippets.

@arathunku
Last active January 4, 2026 12:33
Show Gist options
  • Select an option

  • Save arathunku/b2ee71644f2b07f6f1117d7dbf39832a to your computer and use it in GitHub Desktop.

Select an option

Save arathunku/b2ee71644f2b07f6f1117d7dbf39832a to your computer and use it in GitHub Desktop.
#!/bin/sh
set -e
script=$(readlink -f "$0")
scriptname=$(basename "$script")
[ "$(id -u)" -eq 0 ] || { echo "ERROR: Must be run as root."; exit 1; }
mp=/mnt/root
keyslot_size=32m
swap_size=32G
umount_retry() {
umount "$1" 2>/dev/null || { sleep 2; umount "$1" 2>/dev/null || umount -l "$1" 2>/dev/null; } || true
}
preparation() {
echo "Prepare"
if [ "$efi" = true ]; then
umount_retry /target/boot/efi
fi
umount_retry /target/boot
umount_retry /target/cdrom
umount_retry /target
mkdir -p "$mp"
}
create_subvols() {
echo "Create subvolumes"
mount "/dev/$1" "$mp"
cd "$mp" || { echo "ERROR: Failed to cd to $mp"; exit 1; }
[ -z "$(ls -A home 2>/dev/null)" ] || { echo "ERROR: home directory not empty"; exit 3; }
btrfs subvolume snapshot . @
# Create all subvolumes
btrfs subvolume create @home
btrfs subvolume create @var-cache
btrfs subvolume create @var-log
btrfs subvolume create @var-snap
btrfs subvolume create @usr-local
btrfs subvolume create @nix
btrfs subvolume create @swap
btrfs subvolume create @var-lib-flatpak
btrfs subvolume create @var-lib-docker
btrfs subvolume create @var-lib-snapd
btrfs subvolume create @var-lib-libvirt
btrfs subvolume create .timeline
# Disable CoW for write-heavy subvolumes
chattr +C @var-cache
chattr +C @var-lib-docker
chattr +C @var-lib-libvirt
# Migrate data from @ to subvolumes (includes hidden files)
# Using cp -a to preserve attributes and handle dotfiles, then rm source
migrate_data() {
src="${1:?}"; dst="${2:?}"
if [ -d "$src" ] && [ -n "$(ls -A "$src" 2>/dev/null)" ]; then
cp -a "$src"/. "$dst"/ && rm -rf "${src:?}"/* "${src:?}"/.[!.]* "${src:?}"/..?* 2>/dev/null
fi
}
migrate_data @/home @home
migrate_data @/var/cache @var-cache
migrate_data @/var/log @var-log
migrate_data @/var/snap @var-snap
migrate_data @/usr/local @usr-local
migrate_data @/var/lib/docker @var-lib-docker
migrate_data @/var/lib/flatpak @var-lib-flatpak
migrate_data @/var/lib/snapd @var-lib-snapd
migrate_data @/var/lib/libvirt @var-lib-libvirt
# Remove everything except subvolumes
find . -maxdepth 1 \! -name "@*" \! -name ".*" \! -name . -exec rm -Rf {} \;
btrfs filesystem mkswapfile --size "$swap_size" --uuid clear @swap/swapfile
cd /
umount "$mp"
echo "Mount @ instead of /"
mount "/dev/$1" -o subvol=@ "$mp"
}
encrypt_and_enlarge() {
echo "Encrypt $1"
btrfs filesystem resize "-$keyslot_size" "$mp"
cd /
umount "$mp"
disk=$(lsblk --noheadings --output pkname "/dev/$1")
partition_number=$(lsblk --noheadings --output partn "/dev/$1" | tr -d "[:space:]")
echo 'You may ignore "Warning: keyslot operation could fail ...".'
echo 'See <https://gitlab.com/cryptsetup/cryptsetup/-/issues/896>.'
cryptsetup reencrypt --encrypt --type luks2 --reduce-device-size "$keyslot_size" "/dev/$1"
if [ "$enlarge" = yes ]; then
echo "Enlarge $1"
parted --script "/dev/$disk" resizepart "$partition_number" "100%"
fi
echo "Enter LUKS passphrase to unlock encrypted root:"
cryptsetup open "/dev/$1" root
mount /dev/mapper/root -o subvol=@ "$mp"
btrfs filesystem resize max "$mp"
}
chroot_and_mkinitramfs() {
echo "Prepare $mp for chroot"
mount -t proc proc "$mp/proc"
mount -t sysfs sys "$mp/sys"
mount --bind /dev "$mp/dev"
mount --bind /run "$mp/run"
mount "/dev/$2" "$mp/boot"
if [ -n "$3" ]; then
mount "/dev/$3" "$mp/boot/efi"
fi
cp "$script" "$mp/tmp/$scriptname"
chmod a+x "$mp/tmp/$scriptname"
echo "Chrooting and call the script in the other root"
chroot "$mp" "/tmp/$scriptname" //inner "/dev/$1"
}
unmount_everything() {
echo "Unmounting"
cd /
set +e # Don't exit on error during cleanup
for dir in proc sys dev run; do
umount_retry "$mp/$dir"
done
if [ "$efi" = true ]; then
umount_retry "$mp/boot/efi"
fi
umount_retry "$mp/boot"
umount_retry "$mp"
cryptsetup close root
set -e
}
if [ "$1" = "//inner" ]; then
# echo GRUB_DISABLE_OS_PROBER=false >> /etc/default/grub
root_uuid=$(blkid --output export "$2" | grep ^UUID=) || { echo "ERROR: Failed to get UUID for $2"; exit 1; }
[ -n "$root_uuid" ] || { echo "ERROR: UUID empty for $2"; exit 1; }
opts="defaults,space_cache=v2,ssd,discard=async,noatime"
mkdir -p /nix /swap /var/cache /var/log /var/snap /usr/local \
/var/lib/docker /var/lib/libvirt /var/lib/flatpak /var/lib/snapd
echo "Patch /etc/fstab"
sed --in-place 's!^.* / btrfs defaults 0 1$!/dev/mapper/root / btrfs defaults,space_cache=v2,ssd,discard=async,noatime,subvol=@ 0 0!' /etc/fstab
{
echo "/dev/mapper/root /home btrfs $opts,subvol=@home 0 0"
echo "/dev/mapper/root /nix btrfs $opts,subvol=@nix 0 0"
echo "/dev/mapper/root /swap btrfs $opts,subvol=@swap 0 0"
echo "/dev/mapper/root /var/cache btrfs $opts,subvol=@var-cache 0 0"
echo "/dev/mapper/root /var/log btrfs $opts,subvol=@var-log 0 0"
echo "/dev/mapper/root /var/snap btrfs $opts,subvol=@var-snap 0 0"
echo "/dev/mapper/root /usr/local btrfs $opts,subvol=@usr-local 0 0"
echo "/dev/mapper/root /var/lib/docker btrfs $opts,subvol=@var-lib-docker 0 0"
echo "/dev/mapper/root /var/lib/flatpak btrfs $opts,subvol=@var-lib-flatpak 0 0"
echo "/dev/mapper/root /var/lib/snapd btrfs $opts,subvol=@var-lib-snapd 0 0"
echo "/dev/mapper/root /var/lib/libvirt btrfs $opts,subvol=@var-lib-libvirt 0 0"
echo "/swap/swapfile none swap defaults 0 0"
} >> /etc/fstab
echo "root $root_uuid none luks,discard" > /etc/crypttab
update-grub
echo "Install cryptsetup and update initramfs"
apt-get install -y cryptsetup-initramfs
update-initramfs -u -k all
exit
fi
show_help() {
echo "Add BTRFS subvolumes to a root partition and encrypt it with LUKS2"
echo
echo "Usage: ubuntu-btrfs-setup [options] {root-dev} {boot-dev} [{efi-dev}]"
echo
echo "The devices must be given without the /dev/ prefix."
echo
echo " --enlarge enlarge root partition to maximum"
echo
echo "--enlarge resizes the root partition to 100%. This makes encryption"
echo "much faster, because you make a small partition for just the"
echo "installation of the base system, and then you call this script,"
echo "which has to encrypt only the smaller partition. The enlargement"
echo "covers only empty space, so no encryption is necessary there."
echo
echo "Subvolumes created:"
echo " @, @home, @nix, @swap, @var-cache, @var-log, @var-snap, @usr-local,"
echo " @var-lib-docker, @var-lib-flatpak, @var-lib-snapd, @var-lib-libvirt,"
echo " .timeline"
}
if [ $# -eq 0 ]; then
show_help
exit
fi
enlarge=no
while :; do
case $1 in
-h|-\?|--help)
show_help
exit
;;
--enlarge)
enlarge=yes
;;
--*)
echo "Invalid option \"$1\". Use --help for more information."
exit 2
;;
*)
break
esac
shift
done
if [ -z "$3" ]; then
efi=false
else
efi=true
fi
if [ -z "$1" ] || [ -z "$2" ]; then
echo "You must pass the device of the root, boot, and optionally "
echo "EFI partition (without the /dev/) to this script."
echo
echo "Use --help for more information."
exit 2
fi
# Validate devices exist
[ -b "/dev/$1" ] || { echo "ERROR: Root device /dev/$1 not found"; exit 1; }
[ -b "/dev/$2" ] || { echo "ERROR: Boot device /dev/$2 not found"; exit 1; }
[ -z "$3" ] || [ -b "/dev/$3" ] || { echo "ERROR: EFI device /dev/$3 not found"; exit 1; }
# Validate filesystem types
root_fs=$(blkid -o value -s TYPE "/dev/$1" 2>/dev/null)
[ "$root_fs" = "btrfs" ] || { echo "ERROR: Root must be BTRFS, got: ${root_fs:-none}"; exit 1; }
boot_fs=$(blkid -o value -s TYPE "/dev/$2" 2>/dev/null)
case "$boot_fs" in
ext2|ext3|ext4|vfat) ;;
*) echo "ERROR: Boot must be ext2/3/4 or vfat, got: ${boot_fs:-none}"; exit 1 ;;
esac
if [ -n "$3" ]; then
efi_fs=$(blkid -o value -s TYPE "/dev/$3" 2>/dev/null)
[ "$efi_fs" = "vfat" ] || { echo "ERROR: EFI must be vfat, got: ${efi_fs:-none}"; exit 1; }
fi
preparation
create_subvols "$1"
encrypt_and_enlarge "$1"
chroot_and_mkinitramfs "$1" "$2" "$3"
unmount_everything
echo "Finished"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment