Skip to main content

Triple-Boot Arch + Ubuntu + Windows (Plain Btrfs, No Encryption)

arch linux
ubuntu
windows
linux
installation
triple-boot
btrfs
snapper
hibernation
grub
os-prober
mkinitcpio
zram
exfat
esp
bitlocker
secure boot
shared partition
no encryption
One NVMe drive booting Windows 11, Arch Linux, and Ubuntu 24 LTS, with both Linux roots on plain Btrfs and one shared plain Btrfs /data partition the two Linux distros mount read-write. Windows goes on first so it can’t stomp the bootloader; Windows↔︎Linux exchange uses a plain exFAT partition all three OSes read. Btrfs subvolumes, per-OS zram + NoCOW hibernation, one GRUB on the shared ESP with os-prober chainloading Windows. No passphrase prompts at boot. Secure Boot off, BitLocker off.
Author

Evanns Morales

Published

June 7, 2026

Disclaimer: a reasoned no-encryption build, not yet a live-tested run

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:

Treat it as a blueprint for the pattern, not a substitute for the tool docs.

What this is

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 /data partition 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-prober picks 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.

What you give up by dropping encryption

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:

  1. 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.
  2. Windows can’t read Btrfs. The shared /data is Linux-only; Windows↔︎Linux exchange uses a separate plain exFAT partition.
  3. The RTC clock war. Windows assumes the hardware clock is local time; Linux assumes UTC. We fix it by telling Windows to use UTC.
  4. Fast Startup and “device encryption.” Both interfere with a clean triple-boot; we turn them off.
  5. Secure Boot. Our GRUB isn’t signed with a key the firmware trusts, so we disable it.

How to read the commands in this guide

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
Doing the math for your disk

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.

Why a 1 GiB ESP we create ourselves, not the 100 MiB one Windows makes

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.

Why Windows is left out of the shared /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)
Turn off Secure Boot now, while you’re 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=nvme

Every command below uses /dev/nvme0n1 as a placeholder — substitute your actual device. Partition with cfdisk (GPT label):

cfdisk /dev/nvme0n1

Inside 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:

  1. d on every existing partition until the table is empty. (If prompted for a label type on a blank disk, choose gpt.)
  2. n1GtEFI Systemp1 (shared ESP).
  3. n16MtMicrosoft reservedp2 (MSR; if your cfdisk doesn’t list MSR, see the note below).
  4. n250GtMicrosoft basic datap3 (Windows C:).
  5. n64GtMicrosoft basic datap4 (exFAT exchange).
  6. n150G → leave as Linux filesystemp5 (shared Btrfs data).
  7. n767G → leave as Linux filesystemp6 (Arch root; your half-of-remaining).
  8. n → rest → leave as Linux filesystemp7 (Ubuntu root).
  9. w, then type yes.
If 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
poweroff

Part 3 — Install Windows 11 first

Windows goes on now, into the partition (p3) we reserved, reusing our ESP.

Chapter 3: Run the Windows installer

  1. Flash a Windows 11 install USB (Microsoft’s Media Creation Tool, or the ISO via Rufus in GPT/UEFI mode). Boot it.
  2. 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.
  3. 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.
If the installer refuses 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 /f

Reboot Windows once so it re-reads the clock. From here on, all three OSes agree the hardware clock is UTC.

Why we fix the clock on the Windows side

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 root

Chapter 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 /mnt

Five 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 /mnt

Use 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/sudoers

The 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.

Harden the shared /data and hand it to your user

The shared partition is plain Btrfs that genfstab already recorded. Soft-fail the mount and fix ownership:

# Soft-fail the /data mount (boot continues even if it's briefly missing)
sed -i 's#subvol=/@data#subvol=/@data,nofail,x-systemd.device-timeout=10s#' /etc/fstab

# Hand the shared @data subvolume to your UID-1000 user — a fresh Btrfs subvolume is root-owned,
# so without this the first-boot probe (echo … > /data/probe-arch.txt as your user) is Permission denied.
chown 1000:1000 /data

Ownership is stored in the filesystem, so Ubuntu (also UID 1000) will see these files as its own user automatically — no second chown needed from Ubuntu. The numeric 1000:1000 (not a username) is deliberate: it’s the integer ownership that has to match across both OSes, and it works even though Ubuntu’s user doesn’t exist yet.

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/fstab

uid=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
EOF

Don’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.conf

Set 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 -P

Chapter 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/nvme0n1p6

Resume 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/swapfile

Now open the GRUB defaults file:

nvim /etc/default/grub

Set 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=false

Install 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.cfg

os-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
reboot

Pull 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"

Verify the exchange is shared and the data partition is owned by you

echo "hello from arch" > /exchange/from-arch.txt      # Windows will be able to read this
ls -l /exchange/from-arch.txt                          # owned by user (uid=1000 from the mount options)

echo "from arch" > /data/probe-arch.txt                # Linux-only (Windows can't read Btrfs)
stat -c '%u %g' /data/probe-arch.txt                   # 1000 1000 — confirms the pinned UID

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 row

Test hibernation before installing Ubuntu

sudo systemctl hibernate

Press 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.

Verify hibernation now, while only Arch + Windows exist

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.txt

Boot 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.

Driving it remotely over SSH?

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/bash

Inside 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-minimal

Add 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
reboot
Why reboot 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.

Ubuntu reorders NVRAM to boot its own GRUB first

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/grub

Ubuntu’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-grub
Expected: update-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 image

Part 7 — Build the unified GRUB menu from Arch

All three OSes now exist, but Ubuntu’s install made its GRUB the EFI default — and neither distro’s os-prober reliably auto-detects the other Linux (a Btrfs @-subvolume root isn’t seen on a bare mount). Windows, though, is detected reliably. So we drive the menu from Arch’s GRUB: os-prober supplies the Windows entry, and we add a chainload entry for Ubuntu by hand.

1 — Boot Arch. Ubuntu’s menu won’t list Arch, so reach Arch’s bootloader directly: at power-on, open the firmware one-time boot menu (F12/Esc, vendor-dependent) and pick the GRUB entry (Arch’s; Ubuntu’s is ubuntu, Windows’ is Windows Boot Manager).

2 — Add the Ubuntu chainload entry:

ESP_UUID=$(blkid -s UUID -o value /dev/nvme0n1p1)
echo "$ESP_UUID"        # sanity: a short FAT id like 7F6E-D6AA

sudo tee /etc/grub.d/40_custom >/dev/null <<EOF
#!/bin/sh
exec tail -n +3 \$0
menuentry "Ubuntu 24.04 LTS" {
    insmod part_gpt
    insmod fat
    insmod chain
    search --no-floppy --fs-uuid --set=root $ESP_UUID
    chainloader /EFI/Ubuntu/grubx64.efi
}
EOF
sudo chmod +x /etc/grub.d/40_custom

cat /etc/grub.d/40_custom to confirm the search … line ends with your UUID on one line and the block closes with } (a wrapped paste is the one thing that breaks it), then regenerate so os-prober also picks up Windows:

sudo grub-mkconfig -o /boot/grub/grub.cfg

You should see os-prober report Windows:

Found Windows Boot Manager on /dev/nvme0n1p1@/EFI/Microsoft/Boot/bootmgfw.efi

grub.cfg now lists Arch (direct), Windows Boot Manager (os-prober), and Ubuntu 24.04 LTS (your chainload entry). There’s no “Found Ubuntu” line, and that’s expected — Ubuntu comes from 40_custom, not os-prober.

3 — Make Arch’s GRUB the EFI default:

sudo efibootmgr                            # find the BootNNNN for "GRUB", "Ubuntu", "Windows Boot Manager"
sudo efibootmgr -o <GRUB_NUM>,<UBUNTU_NUM> # Arch's GRUB first; Windows stays reachable via the GRUB menu

Reboot. Arch’s GRUB lists all three: Arch boots directly, Ubuntu chainloads its own GRUB, Windows chainloads its boot manager.

Why Ubuntu is a chainload but Windows isn’t

os-prober finds Windows fine — its boot manager sits on the FAT ESP where os-prober looks. But it detects another Linux by mounting that partition bare and looking for an OS root, and a Btrfs @-subvolume root isn’t there on a bare mount (you get a folder of @, @home, … instead). So Ubuntu gets a hand-written chainload entry, which also never goes stale on a Ubuntu kernel update. To make selecting Ubuntu boot straight through (skip its second GRUB), set GRUB_TIMEOUT=0 + GRUB_TIMEOUT_STYLE=hidden in Ubuntu’s /etc/default/grub, then sudo update-grub.


Part 8 — Final sanity checks across three OSes

The pass condition

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 OSes

From 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.


🚨 Emergency: triple-boot-specific failure modes

If Part 8 passed, skip this. A few Linux-side issues can bite either distro:

  • /data doesn’t mount (you still reach a normal login thanks to nofail; findmnt /data prints nothing): the UUID in /etc/fstab doesn’t match the partition — blkid /dev/nvme0n1p5 and compare, then sudo mount /data.
  • Boot drops to emergency mode: usually a typo on a non-nofail fstab 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.

1. GRUB menu is missing Windows or Ubuntu

No Windows entry (Windows comes from os-prober):

  • os-prober installed and enabled? grep ^GRUB_DISABLE_OS_PROBER= /etc/default/grubfalse; pacman -S os-prober.
  • ESP mounted at /boot when you ran grub-mkconfig? os-prober finds bootmgfw.efi on the ESP. findmnt /boot, then re-run.
  • Windows installed in UEFI mode? Confirm \EFI\Microsoft\Boot\bootmgfw.efi exists: ls /boot/EFI/Microsoft/Boot/.

No Ubuntu entry (Ubuntu comes from your 40_custom chainload, not os-prober): cat /etc/grub.d/40_custom and confirm the search … line ends with the ESP UUID (blkid -s UUID -o value /dev/nvme0n1p1) on one line and the block closes with }; make it executable (sudo chmod +x /etc/grub.d/40_custom), then sudo grub-mkconfig -o /boot/grub/grub.cfg. If selecting Ubuntu lands at grub>, Secure Boot is blocking the unsigned chainload (disable it in firmware), or the path is wrong: ls /boot/EFI/Ubuntu/grubx64.efi.

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 again

Nothing 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

  • nofail keeps a bad exchange partition from blocking boot, but if /exchange is empty, check findmnt /exchange and blkid /dev/nvme0n1p4 (is the fstab UUID correct?).
  • Windows’ Fast Startup leaves filesystems dirty. If Linux mounts /exchange read-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.


Attribution

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.