Triple-Boot Arch + Ubuntu + Windows (Plain Btrfs, No Encryption)
A no-encryption triple-boot blueprint: Windows 11, Arch, and Ubuntu on one drive, with both Linux roots on plain Btrfs and a shared Btrfs /data. It’s a carefully-reasoned procedure, not a command-by-command log I’ve executed end-to-end (yet). The Linux boot path is about as simple as multi-boot gets — /boot sits on a plain ESP that GRUB reads directly. The parts that need the most care are the Windows-integration ones: install order, ESP sharing, the exFAT exchange, the RTC clock fix, and Secure Boot / BitLocker.
Canonical sources to re-read as you go:
- Arch wiki: Dual boot with Windows — ESP sharing,
os-prober, the localtime/UTC clock fix - Arch wiki: GRUB#Detecting other operating systems
- Microsoft’s UEFI/GPT install docs, and the
os-prober/efibootmgrman pages
Treat it as a blueprint for the pattern, not a substitute for the tool docs.
The layout, with no encryption on the Linux side:
- Windows 11, installed first, on its own NTFS partition, so it can’t clobber a bootloader that isn’t there yet.
- Arch Linux and Ubuntu 24 LTS, each on its own plain Btrfs root with the usual subvolumes (
@,@home,@snapshots,@var_log,@swap) — the same root layout as the plain-Btrfs dual-boot. - One shared 150 GiB plain Btrfs
/datapartition the two Linux distros mount read-write. Windows stays out of it (it can’t read Btrfs natively). - One plain exFAT exchange partition that all three OSes read and write, for the Windows handoff.
- Per-OS zram for everyday swap and per-OS NoCOW Btrfs hibernation inside each Linux root, no cross-OS resume collisions.
- One GRUB menu on the shared 1 GiB ESP — Arch’s GRUB, set as the EFI default.
os-proberpicks up the Windows boot manager automatically; Ubuntu gets a hand-written chainload entry (os-prober can’t see its Btrfs@-subvolume root). - Secure Boot off, BitLocker off, the simplest configuration that boots all three reliably.
It stops on the console for the Arch side; Windows and Ubuntu come up graphical out of the box.
Why a no-encryption triple-boot
Same reasoning as the plain-Btrfs dual-boot: for a desktop that never leaves the house, an auto-rebooting box, or a first attempt where you want the multi-boot working before adding crypto, full-disk encryption is friction without much payoff. If you want encryption, you’d wrap each Linux root and the shared /data in LUKS2 — a more involved build, out of scope here. This article is for when you’ve decided you don’t.
With no LUKS on the Linux roots or the shared /data, anyone who can boot a USB stick (or pull the SSD) reads everything on the Linux side, home directories, the shared /data, saved credentials. (Windows’ own files may still be protected if you keep its device encryption, but this article turns BitLocker off to keep the boot path simple, so treat the whole disk as readable-at-rest.) A lost or stolen machine is a data breach, not just a hardware loss. If that’s unacceptable for what lives here, build it with full-disk encryption (LUKS) instead.
Adding Windows introduces five concerns a Linux-only dual-boot doesn’t have:
- Windows is a bad bootloader citizen. It writes its boot manager to the ESP and reorders NVRAM to put itself first. So Windows goes first, and GRUB (installed after) becomes the menu that chainloads it.
- Windows can’t read Btrfs. The shared
/datais Linux-only; Windows↔︎Linux exchange uses a separate plain exFAT partition. - The RTC clock war. Windows assumes the hardware clock is local time; Linux assumes UTC. We fix it by telling Windows to use UTC.
- Fast Startup and “device encryption.” Both interfere with a clean triple-boot; we turn them off.
- Secure Boot. Our GRUB isn’t signed with a key the firmware trusts, so we disable it.
Every command sits in its own copy box, hit the copy icon, paste, run. But watch for angle-bracket placeholders like <USERNAME>, <HOSTNAME>, <TIMEZONE>, <SSID>, or <WIFI_PASSWORD>. Those are values only you know, and they’re written that way on purpose: <…> is invalid shell syntax, so if you blind-paste a line that still has one, it fails loudly instead of silently running with my example values. See a <…>, replace the whole token (brackets and all) before you run the line. Device paths like /dev/nvme0n1p6 are left literal, they match the disk-layout table, so double-check those against your own lsblk once, then they paste cleanly.
Part 1 — The disk layout
Seven partitions on one drive:
| Partition | Size | Filesystem | Mounted on (Linux) | Purpose |
|---|---|---|---|---|
p1 |
1 GiB | FAT32 | /boot |
Shared UEFI ESP (GRUB + Linux kernels + Windows boot manager) |
p2 |
16 MiB | — | — | Microsoft Reserved (MSR) — Windows bookkeeping |
p3 |
~250 GiB | NTFS | (not mounted) | Windows C: |
p4 |
64 GiB | exFAT | /exchange |
Plain exchange — all three OSes read/write |
p5 |
150 GiB | Btrfs | /data |
Shared data — Arch + Ubuntu only |
p6 |
(rest ÷ 2) | Btrfs | / |
Arch root |
p7 |
(rest ÷ 2) | Btrfs | / |
Ubuntu root |
For a 2 TiB SSD, holding back 1 GiB ESP + 16 MiB MSR + 250 GiB Windows + 64 GiB exchange + 150 GiB shared data leaves ≈ 1535 GiB to split between the two Linux roots, roughly 767 GiB each. Adjust every number to taste; nothing downstream depends on the exact counts, only on the order and the partition types.
Let the Windows installer own a blank disk and it creates a 100 MiB ESP, fine for Windows alone, far too small to also hold two Linux distros’ kernels + initramfs in /boot. The whole series puts /boot on the ESP, the natural place for it. So we pre-create the partition table from the Arch live USB first, including a generous 1 GiB ESP, then point the Windows installer at the partition we made. Windows happily reuses an existing ESP of adequate size. This is the single most important ordering trick in the article.
/data
Windows can’t read Btrfs natively, and the third-party drivers (WinBtrfs) are experimental. Instead, Windows gets a clean separation: it never touches /data. The handful of files you move between Windows and Linux go through the plain exFAT exchange partition (p4), which all three OSes read and write with built-in drivers. The tradeoff is explicit: anything on the exchange partition is unencrypted (as is everything else on this disk, in this no-encryption build), treat it as a transfer airlock.
Part 2 — Pre-partition the disk from the Arch live USB
Boot the Arch installer first, only to lay down the partition table (and format the ESP so Windows recognizes it). The Arch install itself happens later, in Part 4, after Windows is on disk.
Chapter 0: Verify the ISO, boot the live USB, confirm UEFI
Same bootstrap as every article in the series. Done it before? Skim to Chapter 1.
On a machine you trust, grab the ISO, its .sig, and sha256sums.txt from the Arch download page into one directory:
cd ~/Downloads/Arch
sha256sum -c sha256sums.txt
# archlinux-2026.05.01-x86_64.iso: OK # bytes match the published checksum
gpg --auto-key-locate clear,wkd -v --locate-external-key pierre@archlinux.org
gpg --verify archlinux-2026.05.01-x86_64.iso.sig archlinux-2026.05.01-x86_64.iso
# gpg: Good signature from "Pierre Schmitz <pierre@archlinux.org>" [unknown]The WARNING: The key's User ID is not certified line is expected. Flash the verified ISO with balenaEtcher or dd (mind of=). Boot it, interrupt boot (F2/F10/F12/Esc), pick the USB, choose “Arch Linux install medium.” Confirm UEFI:
cat /sys/firmware/efi/fw_platform_size
# 64 (32 on very old hardware; if the file is absent you booted BIOS — fix in firmware)Before leaving firmware setup, disable Secure Boot — our GRUB isn’t signed with a key your firmware trusts. While you’re there, set the firmware to UEFI-only (disable CSM/legacy boot) so Windows installs in UEFI/GPT mode, matching Linux.
If you want to SSH in from another machine for comfort: bring up Wi-Fi with iwctl (or plug in Ethernet), then systemctl enable --now sshd, set a root password with passwd, and connect with ssh root@<ip>.
Chapter 1: Lay down the partition table
Confirm the disk first, wiping the wrong one is unrecoverable:
lsblk -d -o NAME,SIZE,MODEL,TRAN # NVMe shows TRAN=nvmeEvery command below uses /dev/nvme0n1 as a placeholder — substitute your actual device. Partition with cfdisk (GPT label):
cfdisk /dev/nvme0n1Inside cfdisk, move the highlight with ↑/↓ (or vim’s j and k) and press Enter to choose. Every new partition starts as Linux filesystem; to set a different type, highlight it, press t, and pick from the list the same way:
don every existing partition until the table is empty. (If prompted for a label type on a blank disk, choose gpt.)n→1G→t→ EFI System →p1(shared ESP).n→16M→t→ Microsoft reserved →p2(MSR; if yourcfdiskdoesn’t list MSR, see the note below).n→250G→t→ Microsoft basic data →p3(Windows C:).n→64G→t→ Microsoft basic data →p4(exFAT exchange).n→150G→ leave as Linux filesystem →p5(shared Btrfs data).n→767G→ leave as Linux filesystem →p6(Arch root; your half-of-remaining).n→ rest → leave as Linux filesystem →p7(Ubuntu root).w, then typeyes.
cfdisk has no “Microsoft reserved” / “Microsoft basic data” types
cfdisk exposes a curated type list; older builds may not. Two options: (a) just make p2/p3/p4 Linux filesystem for now, the Windows installer will re-stamp p3’s type and create the MSR itself; or (b) use gdisk, where you can set exact GPT type GUIDs (0700 Microsoft basic data, 0c01 Microsoft reserved, ef00 EFI system, 8300 Linux filesystem). The MSR is technically optional on UEFI/GPT, reserving 16 MiB now keeps the layout tidy.
Chapter 2: Format the ESP and the exchange partition
Format only the two partitions Windows needs to recognize now. A pre-formatted FAT32 ESP is exactly what makes Windows reuse ours. The exFAT exchange partition we format now too:
mkfs.fat -F32 -n ESP /dev/nvme0n1p1 # shared ESP — Windows + GRUB both live here
mkfs.exfat -n EXCHANGE /dev/nvme0n1p4 # plain exchange (needs exfatprogs, present on the live ISO)Leave p2, p3, p5, p6, p7 unformatted:
p3, the Windows installer formats it NTFS.p5/p6, we format them Btrfs during the Arch install in Part 4.p7, formatted during the Ubuntu install in Part 6.
lsblk -o NAME,SIZE,FSTYPE,PARTTYPENAME /dev/nvme0n1 # sanity-check the table before rebooting
poweroffPart 3 — Install Windows 11 first
Windows goes on now, into the partition (p3) we reserved, reusing our ESP.
Chapter 3: Run the Windows installer
- Flash a Windows 11 install USB (Microsoft’s Media Creation Tool, or the ISO via Rufus in GPT/UEFI mode). Boot it.
- Proceed to “Where do you want to install Windows?” Select
p3(the ~250 GiB Microsoft basic data partition) and click Format (NTFS). Do not delete partitions, and do not let the installer reformat the whole disk, that would destroy the ESP and the space reserved for Linux. - Windows installs into
p3, reuses the existing 1 GiB ESP for\EFI\Microsoft\Boot\bootmgfw.efi, and uses the MSR. Let it reboot and finish OOBE.
p3 (“can’t install to this disk”)
Usually a firmware still in CSM/legacy mode, or a stray boot flag. Re-check that firmware is UEFI-only (Part 2). Because our cfdisk layout already fills the whole disk (no free space left over), Windows can’t carve a separate WinRE/recovery partition — it folds recovery into p3 (C:) instead, which is fine and keeps the p1…p7 numbering stable. Let it install to p3, but never let it delete or reformat p1 (ESP) or the Linux partitions.
Chapter 4: Tame Windows so it triple-boots cleanly
Three settings, all from inside the freshly installed Windows. Skipping these is the #1 source of “my clock is wrong” and “Linux won’t boot after a Windows update.”
Disable BitLocker / device encryption
Windows 11 silently turns on device encryption on a lot of modern hardware, and BitLocker prompts for a recovery key whenever the boot path changes, and installing GRUB is a boot-path change. Turn it off:
- Settings → Privacy & security → Device encryption → Off (wait for decryption to finish), or
- on Pro editions, Control Panel → BitLocker Drive Encryption → Turn off for C:.
Disable Fast Startup
Fast Startup leaves Windows in a hybrid-hibernated state on shutdown, marking the NTFS volume “dirty” and confusing the next OS to boot. Turn it off:
- Control Panel → Power Options → Choose what the power buttons do → Change settings that are currently unavailable → untick “Turn on fast startup” → Save.
Make Windows use UTC for the hardware clock
Without this, your clock jumps by your UTC offset every time you switch OS. From an Administrator Command Prompt or PowerShell:
reg add "HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation" /v RealTimeIsUniversal /t REG_DWORD /d 1 /fReboot Windows once so it re-reads the clock. From here on, all three OSes agree the hardware clock is UTC.
You could set Linux to localtime (timedatectl set-local-rtc 1), but the Arch wiki discourages it, localtime RTC breaks around DST transitions. One registry value on Windows is the clean fix.
Part 4 — Install Arch into p6 (plain Btrfs)
Boot the Arch live USB again. From here the Arch install is the standard plain-Btrfs build — the same Btrfs subvolumes, the shared /data, per-OS hibernation — with one difference: the partitions already exist and Windows is present, so we don’t repartition. Every command is inline below.
Chapter 5: Format the two Arch-side Btrfs filesystems
p5 is the shared data partition; p6 is the Arch root. Leave p7 for Ubuntu. We mkfs.btrfs straight onto the partitions:
mkfs.btrfs -L shared /dev/nvme0n1p5 # shared data
mkfs.btrfs -L arch /dev/nvme0n1p6 # Arch rootChapter 6: Create the Btrfs subvolumes
mount /dev/nvme0n1p6 /mnt
btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@snapshots
btrfs subvolume create /mnt/@var_log
btrfs subvolume create /mnt/@swap
umount /mnt
mount /dev/nvme0n1p5 /mnt
btrfs subvolume create /mnt/@data
umount /mntFive subvolumes on the Arch root, one on the shared partition. @ and @home are split so a system rollback never reverts your home directory; @snapshots is where Snapper writes; @var_log keeps noisy logs out of root snapshots; @swap is isolated for the NoCOW hibernation swap file. @data is the single subvolume on the shared partition.
Chapter 7: Mount everything (including the exchange partition)
mount -o noatime,compress=zstd:3,subvol=@ /dev/nvme0n1p6 /mnt
mkdir -p /mnt/{boot,home,.snapshots,var/log,swap,data,exchange}
mount -o noatime,compress=zstd:3,subvol=@home /dev/nvme0n1p6 /mnt/home
mount -o noatime,compress=zstd:3,subvol=@snapshots /dev/nvme0n1p6 /mnt/.snapshots
mount -o noatime,compress=zstd:3,subvol=@var_log /dev/nvme0n1p6 /mnt/var/log
mount -o noatime,subvol=@swap /dev/nvme0n1p6 /mnt/swap
mount /dev/nvme0n1p1 /mnt/boot
mount -o noatime,compress=zstd:3,subvol=@data /dev/nvme0n1p5 /mnt/data@swap mounts without compression — a swap file must never sit on a compressed mount. We mount /data now so genfstab captures it automatically in the next step, but we don’t mount the exFAT exchange: exFAT needs ownership options Btrfs doesn’t, so we hand-write its fstab line in Chapter 9 instead.
Chapter 8: Pacstrap, fstab, chroot
The package set, plus exfatprogs for the exchange partition:
reflector --country 'United States' --protocol https --latest 20 --age 12 \
--sort rate --save /etc/pacman.d/mirrorlist
pacstrap -K /mnt \
base linux linux-firmware linux-headers intel-ucode \
btrfs-progs exfatprogs \
grub efibootmgr grub-btrfs os-prober \
snapper snap-pac \
networkmanager openssh sudo \
neovim git base-devel \
zram-generator inotify-tools reflector
genfstab -U /mnt >> /mnt/etc/fstab
arch-chroot /mntUse amd-ucode on AMD hardware. The two additions over a single-boot install are os-prober (so GRUB discovers Windows; Ubuntu gets a manual chainload entry in Part 7) and exfatprogs (so Arch can mount the exchange). After genfstab, cat /etc/fstab should show five Arch-root subvolume lines (the same UUID, different subvol=), one line for /data on the shared filesystem’s UUID, and the FAT32 /boot line.
Chapter 9: Configure the system inside the chroot
This chapter covers locale/user (with the pinned UID), the /data hardening, the hibernation swap file, zram, the exchange-partition fstab line, and mkinitcpio. Run the blocks in order.
Locale, time, hostname, user, with a pinned UID 1000
ln -sf /usr/bin/nvim /usr/local/bin/vi
echo 'export EDITOR=nvim' >> /etc/profile
ln -sf /usr/share/zoneinfo/<TIMEZONE> /etc/localtime # e.g. America/Chicago
hwclock --systohc
sed -i 's/^#en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
locale-gen
echo "LANG=en_US.UTF-8" > /etc/locale.conf
echo "KEYMAP=us" > /etc/vconsole.conf
echo "<HOSTNAME>" > /etc/hostname # e.g. arch-triple
cat > /etc/hosts <<'EOF'
127.0.0.1 localhost
::1 localhost
127.0.1.1 arch-triple.localdomain arch-triple
EOF
passwd
groupadd -g 1000 <USERNAME>
useradd -m -u 1000 -g 1000 -G wheel,video,audio,input <USERNAME>
passwd <USERNAME>
sed -i 's/^# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) ALL/' /etc/sudoersThe pinned UID/GID 1000 is what makes file ownership on the shared /data match between Arch and Ubuntu. Linux stores ownership as integers, not names: if the same person is UID 1000 on Arch but 1001 on Ubuntu, /data ownership can’t line up in both directions. Pin 1000 on both sides — use the same username and -u 1000 -g 1000 when you create the Ubuntu user later.
The exchange partition’s fstab line
exFAT has no Unix ownership, so we assign it at mount time. Add the line by hand (it isn’t in genfstab because we didn’t mount it):
echo "UUID=$(blkid -s UUID -o value /dev/nvme0n1p4) /exchange exfat defaults,uid=1000,gid=1000,umask=022,nofail 0 0" \
>> /etc/fstabuid=1000,gid=1000 makes every file on the exchange show up as owned by your user; umask=022 gives sane permissions; nofail means a missing/unformatted exchange partition won’t block boot.
Hibernation swap file + zram
btrfs filesystem mkswapfile --size 36g --uuid clear /swap/swapfile # size ≥ your RAM
echo '/swap/swapfile none swap defaults,pri=-2 0 0' >> /etc/fstab
btrfs inspect-internal map-swapfile -r /swap/swapfile # save the integer → <RESUME_OFFSET>
cat > /etc/systemd/zram-generator.conf <<'EOF'
[zram0]
zram-size = ram / 2
compression-algorithm = zstd
swap-priority = 100
fs-type = swap
EOFDon’t swapon from inside the chroot — activating the swap file from the live USB’s kernel blocks clean unmounting later, and /etc/fstab picks it up on first boot anyway. The two-tier arrangement: zram (priority 100) is compressed swap living in RAM, so it absorbs nearly all everyday memory pressure with zero SSD writes; the NoCOW Btrfs swap file (priority -2) is touched only on hibernation, when the kernel writes the full RAM image out to disk. mkswapfile creates it NoCOW and unfragmented, which is what makes a single, stable resume offset possible.
mkinitcpio
Open the mkinitcpio config:
nvim /etc/mkinitcpio.confSet these two lines:
MODULES=(btrfs)
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck)
MODULES=(btrfs) lets the initramfs read a Btrfs root. The HOOKS line is the standard systemd set; the root mounts directly from p6 at boot. Save, then rebuild:
mkinitcpio -PChapter 10: GRUB, cmdline, os-prober, install
The cmdline needs two machine-specific values: the Arch root Btrfs filesystem UUID (p6) and the swap-file resume offset. Print both now so you can paste them straight in without scrolling back.
Arch root Btrfs filesystem UUID (p6):
blkid -s UUID -o value /dev/nvme0n1p6Resume offset — the byte offset (in pages) of the swap file’s first extent, the same integer you saved when you created the swap file earlier in this part. Re-print it here; the swap file already exists, so this is just a read:
btrfs inspect-internal map-swapfile -r /swap/swapfileNow open the GRUB defaults file:
nvim /etc/default/grubSet the GRUB_CMDLINE_LINUX_DEFAULT line, pasting in the two values you just printed (no angle brackets in the file):
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 quiet zswap.enabled=0 resume=UUID=<ARCH_BTRFS_UUID> resume_offset=<RESUME_OFFSET>"The resume target is the only thing you add by hand — grub-mkconfig auto-detects the root partition and subvol=@ for you.
Enable os-prober so GRUB discovers Windows — its boot manager is reliably detected on the ESP. (Ubuntu doesn’t exist yet; when it does, its Btrfs root won’t auto-detect, so it gets a chainload entry in Part 7.)
sed -i 's/^#GRUB_DISABLE_OS_PROBER=false/GRUB_DISABLE_OS_PROBER=false/' /etc/default/grub
grep '^GRUB_DISABLE_OS_PROBER=' /etc/default/grub # GRUB_DISABLE_OS_PROBER=falseInstall GRUB to the shared ESP and generate the config:
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
grub-mkconfig -o /boot/grub/grub.cfgos-prober already has something to find, Windows:
Found Windows Boot Manager on /dev/nvme0n1p1@/EFI/Microsoft/Boot/bootmgfw.efi
…and grub.cfg will contain an Arch entry plus a Windows Boot Manager entry that chainloads bootmgfw.efi. Ubuntu doesn’t exist yet.
Chapter 11: Enable services, sanity-check, reboot
systemctl enable NetworkManager sshd fstrim.timer reflector.timer \
snapper-timeline.timer snapper-cleanup.timer grub-btrfsd.service
# quick sanity pass
grep '^MODULES=' /etc/mkinitcpio.conf
grep '^HOOKS=' /etc/mkinitcpio.conf
grep '^GRUB_CMDLINE_LINUX_DEFAULT=' /etc/default/grub
grep '^GRUB_DISABLE_OS_PROBER=' /etc/default/grub
cat /etc/fstab | grep -E 'subvol=|swap|/data|exchange'Leave the chroot and reboot:
exit
umount -R /mnt
rebootPull the USB. Expected first boot: firmware → GRUB menu (listing Arch Linux and Windows Boot Manager) → pick Arch → kernel/initramfs → @ mounts as / → /data and /exchange mount, zram comes up → login. If you drop into emergency mode, jump to the 🚨 emergency section.
Part 5 — Arch first boot: verify all three partitions, snapper, hibernation
Log in as your user:
findmnt / # subvol=/@, on /dev/nvme0n1p6
findmnt /home # subvol=/@home
findmnt /data # subvol=/@data, on /dev/nvme0n1p5
findmnt /exchange # exfat, on /dev/nvme0n1p4
swapon --show # zram0 (pri 100) + /swap/swapfile (pri -2)Reconnect Wi-Fi, re-enable NTP, verify the timezone:
nmcli radio wifi on
nmcli device wifi connect "<SSID>" password "<WIFI_PASSWORD>"
ping -c 3 ping.archlinux.org
sudo timedatectl set-ntp true
timedatectl status # correct Time zone, "System clock synchronized: yes", "RTC in local TZ: no"Configure snapper for /
A plain create-config collides with the @snapshots we already mounted; the Arch-recommended dance avoids it:
sudo umount /.snapshots
sudo rm -rf /.snapshots
sudo snapper -c root create-config /
sudo btrfs subvolume delete /.snapshots
sudo mkdir /.snapshots
sudo chmod 750 /.snapshots
sudo mount -a
sudo snapper -c root set-config \
TIMELINE_LIMIT_HOURLY=5 TIMELINE_LIMIT_DAILY=7 \
TIMELINE_LIMIT_WEEKLY=2 TIMELINE_LIMIT_MONTHLY=1 TIMELINE_LIMIT_YEARLY=0
sudo snapper -c root list # at least one rowTest hibernation before installing Ubuntu
sudo systemctl hibernatePress power to resume; expect your exact session restored (no passphrase prompt). If it boots fresh, re-check resume=/resume_offset= and re-run mkinitcpio -P && grub-mkconfig -o /boot/grub/grub.cfg.
Fixing hibernation after Ubuntu has rewritten parts of the boot path is meaningfully harder. Confirm it works here.
Part 6 — Install Ubuntu 24 LTS into p7
We build Ubuntu’s root by hand with debootstrap, into p7. Windows being present changes nothing about the Ubuntu steps; the unified menu gets built from Arch’s GRUB in Part 7.
From the Arch side, snapshot the current NVRAM so you can compare after Ubuntu reorders it:
efibootmgr -v > ~/efi-before-ubuntu.txtBoot a verified Ubuntu 24.04 LTS Desktop USB and pick “Try Ubuntu”, then build Ubuntu’s root by hand from the live session with debootstrap, using p7 as Ubuntu’s root.
The Ubuntu Desktop live image has no SSH server and logs you in as a passwordless ubuntu user, so set it up first: sudo apt install -y openssh-server, passwd ubuntu (give the live user a password), sudo systemctl enable --now ssh, then from the other machine ssh ubuntu@<ip> and sudo -i to become root.
Open a terminal, sudo -i, apt update && apt install -y debootstrap, then:
# 1 — format p7 as Btrfs + subvolumes
mkfs.btrfs -L ubuntu /dev/nvme0n1p7
mount /dev/nvme0n1p7 /mnt
btrfs subvolume create /mnt/@ && btrfs subvolume create /mnt/@home && umount /mnt
# 2 — mount target + shared ESP
mount -o noatime,compress=zstd:3,subvol=@ /dev/nvme0n1p7 /mnt
mkdir -p /mnt/home /mnt/boot/efi
mount -o noatime,compress=zstd:3,subvol=@home /dev/nvme0n1p7 /mnt/home
mount /dev/nvme0n1p1 /mnt/boot/efi # shared ESP — do not reformat
# 3 — bootstrap + fstab (root, home, ESP, shared /data)
debootstrap --arch amd64 noble /mnt http://archive.ubuntu.com/ubuntu
UB=$(blkid -s UUID -o value /dev/nvme0n1p7)
ESP=$(blkid -s UUID -o value /dev/nvme0n1p1)
SHARED=$(blkid -s UUID -o value /dev/nvme0n1p5)
printf 'UUID=%s / btrfs noatime,compress=zstd:3,subvol=@ 0 0\nUUID=%s /home btrfs noatime,compress=zstd:3,subvol=@home 0 0\nUUID=%s /boot/efi vfat umask=0077 0 1\nUUID=%s /data btrfs rw,noatime,compress=zstd:3,subvol=@data,nofail,x-systemd.device-timeout=10s 0 0\n' "$UB" "$UB" "$ESP" "$SHARED" >> /mnt/etc/fstab
mkdir -p /mnt/data
# 4 — chroot
cp /etc/resolv.conf /mnt/etc/resolv.conf
for d in dev dev/pts proc sys run; do mount --rbind /$d /mnt/$d 2>/dev/null || mount --bind /$d /mnt/$d; done
chroot /mnt /bin/bashInside the chroot, widen the sources list, install the system (a plain grub-install onto the shared ESP), and create the UID-1000 user (root device here is /dev/nvme0n1p7):
cat > /etc/apt/sources.list <<EOF
deb http://archive.ubuntu.com/ubuntu noble main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu noble-updates main restricted universe multiverse
deb http://security.ubuntu.com/ubuntu noble-security main restricted universe multiverse
EOF
apt update
apt install -y ubuntu-standard linux-image-generic grub-efi-amd64 btrfs-progs network-manager sudo locales tzdata exfatprogs
update-initramfs -u -k all
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu --recheck
update-grub # builds Ubuntu's own GRUB menu
groupadd -g 1000 <USERNAME>; useradd -m -u 1000 -g 1000 -s /bin/bash -G sudo,adm,cdrom,dip,plugdev <USERNAME> && passwd <USERNAME>
apt install -y ubuntu-desktop-minimalAdd the exchange-partition fstab line (the shared /data line is already in fstab from step 3), then exit and reboot:
echo "UUID=$(blkid -s UUID -o value /dev/nvme0n1p4) /exchange exfat defaults,uid=1000,gid=1000,umask=022,nofail 0 0" >> /etc/fstab
mkdir -p /exchange
update-grub
exit
sync
rebootreboot instead of unmounting by hand
The mount --rbind /run /mnt/run from entering the chroot is recursive, it pulls /run/user/0 and the live session’s snap mount namespaces in under /mnt/run, so umount -R … tends to fail with target is busy. Everything’s on disk; sync flushes and reboot tears down every mount the hard way.
We undo that in Part 7 — booting Arch via the firmware menu, then setting Arch’s GRUB first with efibootmgr — because Arch’s GRUB is the one that gets the unified menu.
Chapter 12: Add Ubuntu’s hibernation swap file
Boot Ubuntu from the GRUB menu, log in, and mirror Arch’s per-OS hibernation:
findmnt / # /dev/nvme0n1p7
sudo btrfs subvolume create /swap
sudo btrfs filesystem mkswapfile --size 36g --uuid clear /swap/swapfile
echo '/swap/swapfile none swap defaults,pri=-2 0 0' | sudo tee -a /etc/fstab
sudo btrfs inspect-internal map-swapfile -r /swap/swapfile # save → <UBUNTU_RESUME_OFFSET>Point Ubuntu’s cmdline and initramfs at its own Btrfs filesystem (never Arch’s). The resume offset is the integer you printed just above with map-swapfile; print the Ubuntu root UUID too:
sudo blkid -s UUID -o value /dev/nvme0n1p7 # this is <UBUNTU_BTRFS_UUID>Open the GRUB defaults file as root. sudoedit uses your $EDITOR, and a fresh Ubuntu hasn’t set one — so it opens nano (edit, then Ctrl-O, Enter to save and Ctrl-X to exit). Prefer nvim? Run sudo apt install -y neovim && export EDITOR=nvim first.
sudoedit /etc/default/grubUbuntu’s GRUB_CMDLINE_LINUX_DEFAULT typically reads quiet splash. Add resume= and resume_offset=, pasting in the two values (no angle brackets in the file):
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash resume=UUID=<UBUNTU_BTRFS_UUID> resume_offset=<UBUNTU_RESUME_OFFSET>"
Tell Ubuntu’s initramfs about the resume device, then rebuild both the initramfs and GRUB:
echo "RESUME=UUID=<UBUNTU_BTRFS_UUID>" | sudo tee /etc/initramfs-tools/conf.d/resume
sudo update-initramfs -u -k all
sudo update-grubupdate-initramfs warns “no matching swap device is available”
You’ll see this, and it’s harmless with a swap file:
W: initramfs-tools configuration sets RESUME=UUID=<uuid>
W: but no matching swap device is available.
initramfs-tools validates RESUME= by looking for a swap partition whose UUID matches. Your swap is a file inside the Btrfs filesystem <uuid>, not a separate swap device, so there’s nothing for it to match. What actually performs the resume is the kernel cmdline resume=UUID=… resume_offset=… you set in /etc/default/grub: the kernel resolves the UUID and seeks to the offset. The real test is to actually hibernate (next step) — the swap file also isn’t active in this just-created session yet, fstab activates it on the next boot.
Confirm Ubuntu sees the shared and exchange partitions, then test hibernation:
findmnt /data /exchange
swapon --show # zram + /swap/swapfile
ls -l /data /exchange # files written from Arch appear, owned by user
sudo systemctl hibernate # resumes into Ubuntu, separate from Arch's imagePart 8 — Final sanity checks across three OSes
All three boot from one GRUB menu; Arch and Ubuntu each hibernate into themselves and mount the shared /data with identical ownership; every OS reads/writes /exchange; Windows boots, keeps correct time, and never prompts for a BitLocker key on a normal boot.
From Arch / Ubuntu (run on each):
findmnt / /home /data /exchange
swapon --show # zram + that OS's own /swap/swapfile
sudo systemctl hibernate # resumes into the same OS
stat -c '%u %g %n' /data/* # all 1000 — pinned-UID parity
ls -l /exchange # files from all three OSesFrom Windows: confirm the clock is correct after switching from Linux (proves the RealTimeIsUniversal fix), and that File Explorer → the EXCHANGE volume shows the files Linux wrote. Drop a file there and confirm Linux sees it on the next boot.
If Part 8 passed, skip this. A few Linux-side issues can bite either distro:
/datadoesn’t mount (you still reach a normal login thanks tonofail;findmnt /dataprints nothing): the UUID in/etc/fstabdoesn’t match the partition —blkid /dev/nvme0n1p5and compare, thensudo mount /data.- Boot drops to emergency mode: usually a typo on a non-
nofailfstab line (/or/home). Boot the Arch USB,mount -o subvol=@ /dev/nvme0n1p6 /mnt,mount /dev/nvme0n1p1 /mnt/boot,arch-chroot /mnt, fix/etc/fstab, reboot. - Hibernation resumes into the other OS: each OS’s
resume=must point at its own Btrfs filesystem UUID (Arch =p6, Ubuntu =p7), never the other’s.
The Windows-specific failures are below.
2. A Windows update stole the boot order (boots straight to Windows)
Boot Linux (force the firmware boot menu once), then:
sudo efibootmgr # note the GRUB and Windows BootNNNN numbers
sudo efibootmgr -o <GRUB_NUM>,<WINDOWS_NUM> # put GRUB first againNothing was lost, only the order changed. GRUB still chainloads Windows fine.
3. Clock is wrong after switching OS
The RealTimeIsUniversal registry value (Part 3, Chapter 4) wasn’t set or didn’t take. Re-apply it from an Administrator prompt in Windows and reboot Windows once. Do not set Linux to localtime, that just moves the bug.
4. Windows demands a BitLocker recovery key after you installed GRUB
Device encryption was still on when the boot path changed. Enter the recovery key (from aka.ms/myrecoverykey), then turn device encryption off (Part 3, Chapter 4) so it stops re-prompting.
5. The exchange partition won’t mount / shows no files
nofailkeeps a bad exchange partition from blocking boot, but if/exchangeis empty, checkfindmnt /exchangeandblkid /dev/nvme0n1p4(is thefstabUUID correct?).- Windows’ Fast Startup leaves filesystems dirty. If Linux mounts
/exchangeread-only or refuses, you almost certainly skipped disabling Fast Startup, do it (Part 3, Chapter 4), fully shut Windows down (not restart), and remount.
Part 9 — Closing the loop
You now have a single drive that boots Windows, Arch, and Ubuntu, with two plain-Btrfs Linux roots, a shared /data the two Linux distros mount as one home for documents, a plain exFAT exchange partition for the Windows handoff, per-OS hibernation that never collides, and one GRUB menu, all without a single passphrase prompt or a single byte of LUKS to go wrong at boot.
The tradeoff you made is the one Part 1 spelled out: nothing on this disk is encrypted at rest. That’s the right call for a stationary, physically-secured machine and the wrong one for a laptop. If the machine’s role changes, adding encryption means rebuilding on LUKS2 — the same layout, but with a LUKS2 container around each Linux root and the shared /data, plus the extra boot-path setup an encrypted disk needs. There’s no in-place way to add it; decide up front.
The desktop layer for the Arch side is still a separate choice, a custom Hyprland is the direction I’m taking (upcoming), and it doesn’t touch any of the multi-boot machinery below it.
The Arch Linux logo is a trademark of the Arch Linux project and is used here under Arch Linux’s branding terms for editorial purposes only. The Ubuntu wordmark and Circle of Friends are trademarks of Canonical Ltd.; the Windows name and logo are trademarks of Microsoft Corporation, references here are editorial and follow each owner’s trademark policy.