Dual-Boot Arch + Ubuntu (Plain Btrfs, No Encryption) — With a Shared Data Partition
@, @home, @snapshots, @var_log, @swap), per-OS zram + NoCOW Btrfs hibernation, a pinned-UID shared /data, and one GRUB menu on the shared ESP (Arch’s GRUB chainloads Ubuntu). No passphrase prompts — boot goes straight from GRUB to login.
This is a no-encryption dual-boot: two plain Btrfs roots and a shared Btrfs /data. I set it up on real hardware on 2026-06-12 — Arch and Ubuntu 24.04 sharing one ESP and a Btrfs /data, a single GRUB menu, per-OS hibernation, no encryption anywhere — and the steps below are the ones that actually worked. They fold in everything the live install surfaced: the chainload GRUB menu (os-prober can’t see a Btrfs @-subvolume root), the Ubuntu swap-file resume warning, the recordfail tweak for a seamless menu, and the nano-not-nvim editor note. The boot path stays about as simple as multi-boot gets: /boot sits on a plain ESP, and GRUB reads its config and kernels directly off it.
Canonical sources to keep handy as you go:
btrfs— subvolumes, swap files, mount options- Arch wiki: GRUB#Dual booting and
os-prober - Arch wiki: Btrfs and Snapper
- Ubuntu wiki: Installation/FromUSBStick
Your hardware, device names, and partition sizes will differ from mine, so read the tool docs alongside as you go.
The layout, with no encryption anywhere on the disk:
- Two plain Btrfs root partitions — Arch in its own Btrfs partition, Ubuntu in its own, each carrying the usual subvolumes (
@,@home, …). - One shared 150 GiB plain Btrfs data partition, mounted at
/dataon both distros, owned by your user on both sides. Just anfstabUUID line on each OS. - Per-OS everyday swap via zram (compressed RAM, no SSD writes) and per-OS hibernation to a NoCOW Btrfs swap file inside each distro’s own root, no shared swap partition, no cross-OS resume-image collisions.
- One GRUB menu on the shared ESP. Arch’s GRUB is the primary chooser (set as the EFI default), listing Arch directly plus a hand-written chainload entry that boots Ubuntu’s GRUB. We don’t lean on
os-proberto auto-detect the other Linux — it doesn’t reliably see a Btrfs@-subvolume root, so the cross-entry is wired by hand (Part 5).
It stops on the console for the Arch side; the desktop layer is your choice. Ubuntu’s GNOME comes up out of the box.
Why a no-encryption version at all
Full-disk encryption is the right default for a laptop that leaves the house. But there are real cases where it’s the wrong tradeoff:
- A desktop or homelab box that never moves. Physical-theft is your threat model with FDE; a machine bolted in a rack at home has a very different one. The boot-time passphrase is then pure friction.
- Headless / auto-rebooting machines. Anything that has to come back up unattended after a power blip can’t sit at a passphrase prompt. (TPM-bound unlock exists, but that’s more complexity than this article, not less.)
- Learning the layout first. The encryption is the part most likely to break a first attempt. Getting the dual-boot, the shared
/data, snapper, and hibernation working without LUKS in the way is a clean way to build the model before adding crypto back.
If you want encryption, you’d wrap each root and the shared partition in LUKS2 — a different, more involved build, and out of scope here. This article is for when you’ve decided you don’t.
Be honest with yourself about the threat model. With no LUKS:
- Anyone who can boot from a USB stick (or pull the SSD) reads everything — your home directory, the shared
/data, saved credentials, browser sessions. There’s no passphrase gating any of it. - A lost or stolen machine is a data breach, not just a hardware loss.
- The shared
/datahere is plain Btrfs; “shared” no longer implies “encrypted at rest.”
If any of that is unacceptable for what lives on this machine, stop and build it with full-disk encryption (LUKS) instead. Everything below assumes you’ve consciously chosen an unencrypted disk.
What adding Ubuntu introduces:
- Who owns GRUB? Both distros install their own bootloader to the same ESP. We make Arch’s GRUB the primary menu (via EFI boot order) and give it a manual chainload entry that hands off to Ubuntu’s GRUB.
os-proberauto-detection is unreliable here — a Btrfs@-subvolume root often isn’t seen — so we wire the cross-entry by hand. - UID/GID coordination for the shared partition. If
useris UID 1000 on Arch and UID 1001 on Ubuntu, every file on/datalooks like it belongs to a different user depending on which OS you booted. We pin UID 1000 on both sides. - Resume-image collisions. Two OSes hibernating to the same swap target would overwrite each other’s image. We give each OS its own NoCOW Btrfs swap file inside its own root, plus zram for everyday swap.
- Reaching Arch after Ubuntu installs. Ubuntu’s installer makes its own GRUB the EFI default and won’t list Arch. So you boot Arch once via the firmware boot menu, build the unified menu there, and set Arch’s GRUB first in the EFI order (Part 5).
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/nvme0n1p3 are left literal, they match the disk-layout table, so double-check those against your own lsblk once, then they paste cleanly.
Part 1 — Bootstrap the Arch live environment
Chapter 0: Verify the ISO, boot the live USB, get online
Download and verify the ISO
On a machine you trust, grab three files from the Arch download page — I keep them together in ~/Downloads/Arch:
- the ISO (
archlinux-2026.05.01-x86_64.iso, ~1.4 GiB), - its signature (
….iso.sig), - the
sha256sums.txtchecksum file.
Checksum, confirms the bytes on your disk match what the mirror published:
cd ~/Downloads/Arch
sha256sum -c sha256sums.txt
# archlinux-2026.05.01-x86_64.iso: OKSignature, confirms the ISO was actually published by the Arch release engineer, not a tampered mirror:
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]gpg: WARNING: The key's User ID is not certified with a trusted signature!
Expected. It means you haven’t personally signed Pierre’s key into your local web of trust, the signature itself is still valid, gpg just can’t prove the key’s ownership. Fine for a one-off install.
Flash the verified ISO to a USB stick with balenaEtcher (pick image → pick drive → flash) or dd if you’re comfortable (be very careful with of=).
Boot the install media (and confirm UEFI)
Plug the USB into the target machine, interrupt boot (F2/F10/F12/Esc depending on vendor), pick the USB, and choose the default “Arch Linux install medium” entry. At the root shell, confirm you booted UEFI, not legacy BIOS, everything below (the ESP, GRUB) assumes UEFI:
cat /sys/firmware/efi/fw_platform_size
# 64 (or 32 on very old hardware)If that file doesn’t exist, you’re in BIOS mode, fix it in firmware settings before continuing. If the console font is too small on a high-DPI panel, setfont ter-132b.
(Optional but recommended) SSH in from another computer
A dual-boot install is long and reference-heavy; doing it over SSH lets you keep this guide open on one screen and paste commands on the other. First get the live USB online with iwctl:
iwctldevice list
station wlan0 scan
station wlan0 get-networks
station wlan0 connect "<SSID>"
Quote any SSID with spaces; substitute wlan0 with your actual device from device list; exit when connected. Confirm connectivity:
ping -c 3 ping.archlinux.orgFind the address to SSH into:
ip -brief address # look at the wlan0 line for inet x.x.x.x/yyMake sure the SSH daemon is up, set a temporary live-environment root password, then connect from your other machine:
systemctl enable --now sshd
passwd # live-USB-only password; the installed system gets its own later
# from the other computer:
ssh root@<ip-from-above>Keymap, timezone, time sync
loadkeys us # your console keymap
timedatectl set-timezone <TIMEZONE> # e.g. America/Chicago
timedatectl set-ntp true # sync the clock over the networkset-ntp true matters even for a 30-minute install
timedatectl set-ntp true points systemd-timesyncd at network time servers. If the live environment’s clock drifts far enough from real time, TLS handshakes and pacman operations fail with confusing certificate errors. It’s free insurance, always turn it on. (You’ll turn it on again inside the installed system at first boot; the two clocks are independent.)
lsblk -d -o NAME,SIZE,MODEL,TRANNVMe will show TRAN=nvme. Every command below uses /dev/nvme0n1 as a placeholder — substitute your actual device. Wiping the wrong disk is unrecoverable, and with no encryption there’s no second factor protecting the data either way.
Chapter 1: The four-partition layout
This is the central design decision. Four partitions on one drive:
| Partition | Size | Filesystem | Mounted on | Purpose |
|---|---|---|---|---|
p1 |
1 GiB | FAT32 | /boot |
Shared UEFI ESP (kernels, initramfs, GRUB) |
p2 |
150 GiB | Btrfs | /data |
Shared data partition |
p3 |
(remaining ÷ 2) | Btrfs | / |
Arch root |
p4 |
(remaining ÷ 2) | Btrfs | / |
Ubuntu root |
We’re allocating 150 GiB for shared data and splitting the rest 50/50 between Arch and Ubuntu. For a 2 TiB SSD, that’s:
2000 GiB (drive)
− 1 GiB (ESP)
− 150 GiB (shared data)
= 1849 GiB remaining
÷ 2
≈ 924 GiB each for Arch and Ubuntu
Adjust the split up or down based on which OS you spend more time in. Nothing downstream depends on the specific gigabyte counts.
zram (Chapter 9) carries ~all everyday memory pressure on either OS. Hibernation writes to a per-OS NoCOW Btrfs swap file inside each root (Chapter 8 for Arch; Chapter 13 for Ubuntu), sized ≥ RAM and owned by the right OS, so there’s no cross-boundary collision. A shared swap partition would only help a workload that exceeds RAM + zram and isn’t hibernation, a narrow niche. Start without one; add it later by shrinking a root if a real workload forces it.
Partition with cfdisk:
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.n→1G→t→ EFI System →/dev/nvme0n1p1.n→150G→ leave as Linux filesystem →/dev/nvme0n1p2(shared data).n→924G→ leave as Linux filesystem →/dev/nvme0n1p3(Arch root; substitute your half-of-remaining number).n→ rest → leave as Linux filesystem →/dev/nvme0n1p4(Ubuntu root).w, then typeyes.
Verify:
lsblkYou should see four children of nvme0n1, in order, with the sizes you typed.
Chapter 2: Format the partitions
We format the Btrfs filesystems directly on the partitions. Format the ESP, the shared data partition, and the Arch root now; leave p4 for the Ubuntu install in Part 4.
mkfs.fat -F32 /dev/nvme0n1p1 # ESP — must be FAT32 so UEFI/GRUB can read it
mkfs.btrfs -L shared /dev/nvme0n1p2 # shared data
mkfs.btrfs -L arch /dev/nvme0n1p3 # Arch rootEach mkfs.btrfs runs directly on the raw partition, so the device name is simply the partition itself. The -L labels (shared, arch) are cosmetic, but they make later lsblk -f and blkid output easy to read at a glance.
Chapter 3: Create the Btrfs subvolumes on the Arch root
Create the five subvolumes on the Arch root. The kernel doesn’t care about the names, but snapper does:
mount /dev/nvme0n1p3 /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 /mntClick to expand: what each subvolume is for
| Subvolume | Mountpoint | Why it’s separate |
|---|---|---|
@ |
/ |
The Arch root filesystem. Snapshotted by snapper. |
@home |
/home |
Separate so a root rollback doesn’t roll back the user’s home directory. |
@snapshots |
/.snapshots |
Where snapper writes snapshots. Must be its own subvolume. |
@var_log |
/var/log |
Logs are append-mostly and would noisy-up every snapshot. |
@swap |
/swap |
Dedicated subvolume for the hibernation swap file (NoCOW; see Chapter 8). |
And one subvolume on the shared data partition:
mount /dev/nvme0n1p2 /mnt
btrfs subvolume create /mnt/@data
umount /mntA single @data is enough; lay it out as a subvolume from day one so you can add snapshots later without restructuring.
What most people mean by “separate / and /home” in 2026 is a system-rollback shouldn’t stomp my home directory, and /home should grow freely without re-partitioning. Both are exactly what Btrfs subvolumes give you, they share the underlying filesystem’s free-space pool (so neither side runs out before the disk does) but are independent units for snapshotting, rollback, and quota. The old separate-/home-partition pattern was an ext4-era workaround for features Btrfs has natively.
Chapter 4: Mount everything for pacstrap
Mount the Arch root subvolumes with the right options:
mount -o noatime,compress=zstd:3,subvol=@ /dev/nvme0n1p3 /mnt
mkdir -p /mnt/{boot,home,.snapshots,var/log,swap,data}
mount -o noatime,compress=zstd:3,subvol=@home /dev/nvme0n1p3 /mnt/home
mount -o noatime,compress=zstd:3,subvol=@snapshots /dev/nvme0n1p3 /mnt/.snapshots
mount -o noatime,compress=zstd:3,subvol=@var_log /dev/nvme0n1p3 /mnt/var/log
mount -o noatime,subvol=@swap /dev/nvme0n1p3 /mnt/swap
mount /dev/nvme0n1p1 /mnt/boot
mount -o noatime,compress=zstd:3,subvol=@data /dev/nvme0n1p2 /mnt/dataThe @swap subvolume mounts without compression, a swap file must not be compressed. We mount /mnt/data now so genfstab -U /mnt in the next chapter captures the shared partition automatically — that’s the only /data setup needed on the Arch side here (plus a one-line fstab hardening and a chown in Chapter 7).
Chapter 5: Pacstrap, fstab, chroot
Refresh the mirror list, then pacstrap the base system and the dual-boot tooling:
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 \
grub efibootmgr grub-btrfs \
snapper snap-pac \
networkmanager openssh sudo \
neovim git base-devel \
zram-generator \
inotify-tools \
reflectorUse amd-ucode instead of intel-ucode on AMD hardware. grub-btrfs is what lets GRUB boot directly into Btrfs snapshots later.
Generate fstab and enter the new system:
genfstab -U /mnt >> /mnt/etc/fstab
arch-chroot /mntcat /etc/fstabYou should see:
- Five Btrfs lines for the Arch root subvolumes (
/,/home,/.snapshots,/var/log,/swap), all using the sameUUID=of the Arch Btrfs filesystem, each with its ownsubvol=@…. - One Btrfs line for
/datausing the shared Btrfs filesystem’s UUID. - One FAT32 line for
/boot.
The two distinct Btrfs UUIDs (Arch root vs. shared data) confirm genfstab saw them as separate filesystems. If you see only one Btrfs UUID, you skipped a mount somewhere; re-run Chapter 4. These UUIDs are the partitions’ own Btrfs filesystem UUIDs — that’s the only kind of UUID in this layout, so there’s nothing else to keep them straight against.
Part 2 — Inside the chroot
Chapter 6: Locale, time, hostname, user, with a pinned UID
The one dual-boot-specific change here is pinning the user’s UID and GID to 1000 explicitly. We pin the Ubuntu user to 1000 the same way later, so files on /data are owned by the same user-as-the-kernel-sees-it on both OSes. 1000 is the conventional first-user ID on both distros, but we set it by hand rather than assume it.
# Editor + EDITOR var
ln -sf /usr/bin/nvim /usr/local/bin/vi
echo 'export EDITOR=nvim' >> /etc/profile
# Timezone — substitute your zone
ln -sf /usr/share/zoneinfo/<TIMEZONE> /etc/localtime # e.g. America/Chicago
hwclock --systohc
# Locale
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
# Hostname (suffix-based so Arch and Ubuntu show up distinctly in logs)
echo "<HOSTNAME>" > /etc/hostname # e.g. arch-dual
# /etc/hosts
cat > /etc/hosts <<'EOF'
127.0.0.1 localhost
::1 localhost
127.0.1.1 arch-dual.localdomain arch-dual
EOF
# Root password
passwd
# User account — UID and GID pinned to 1000 (matches Ubuntu's default first-user UID)
groupadd -g 1000 <USERNAME>
useradd -m -u 1000 -g 1000 -G wheel,video,audio,input <USERNAME>
passwd <USERNAME>
# sudo for wheel
sed -i 's/^# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) ALL/' /etc/sudoersReplace <USERNAME> with whatever username you want; the only thing that matters is that you use the same username and the same -u 1000 -g 1000 when you create the user during Ubuntu install later.
Linux filesystems store file ownership as integers, not names. If user is UID 1000 on Arch and UID 1001 on Ubuntu, the ownership on /data literally cannot match in both directions, somebody’s ls -l /data will show 1001. Pin the UID at user-create time on both sides and the problem disappears before it starts.
If you forget to pin during Ubuntu install and the user ends up as UID 1001, the recovery is usermod -u 1000 <USERNAME> && groupmod -g 1000 <USERNAME> && find / -uid 1001 -exec chown 1000 {} +, workable but tedious. Pin in advance.
Chapter 7: Harden the /data fstab line and hand it to your user
Sharing /data between the two distros is two small edits to a line that’s already in fstab — the shared partition is a plain Btrfs filesystem that genfstab recorded when you generated the file back in Chapter 5.
First, harden the generated /data line. genfstab wrote it as a hard mount; a hard mount of a device that’s briefly missing fails local-fs.target → emergency mode. Add nofail (boot continues even if /data can’t mount) and a short device timeout (don’t hang 90 s waiting):
sed -i 's#subvol=/@data#subvol=/@data,nofail,x-systemd.device-timeout=10s#' /etc/fstab
grep ' /data ' /etc/fstab
# UUID=<shared-btrfs> /data btrfs rw,noatime,compress=zstd:3,subvol=/@data,nofail,x-systemd.device-timeout=10s 0 0nofail is cheap insurance
nofail matters for any secondary mount: if the shared partition is ever absent (a future repartition, a failing drive, a typo’d UUID), a plain fstab line would fail local-fs.target and drop you to an emergency shell at boot. With nofail, the boot continues and you get a normal login you can debug from. One word of cheap insurance.
Now hand /data to your user. A freshly-mkfs’d Btrfs subvolume is owned by root:root, so as things stand only root can write to /data. Hand the @data subvolume to the user you pinned to UID 1000 in Chapter 6, do it now, inside the chroot, while /data is mounted:
chown 1000:1000 /dataUsing 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. Because ownership is stored in the filesystem, Ubuntu (also UID 1000) will see these files as owned by its user automatically, you won’t have to chown again from Ubuntu. Skip this and the first-boot probe (echo … > /data/probe-arch.txt as your user) fails with Permission denied.
Chapter 8: Hibernation swap file for Arch
zram (Chapter 9) handles everyday compressed swap; hibernation goes to a NoCOW swap file inside the Arch root, so the resume target is unambiguous and there’s no collision with Ubuntu’s own hibernation swap file.
btrfs filesystem mkswapfile --size 36g --uuid clear /swap/swapfile--size 36g matches the 36 GiB RAM I’m sizing this install for. Substitute your RAM size with a small margin (or skip this whole chapter if you don’t want hibernation, then resume= and resume_offset= drop out of the GRUB cmdline in Chapter 10).
swapon from inside the chroot
Activating the swap file from the live USB’s kernel prevents clean unmounting later. /etc/fstab will pick it up automatically on first boot of the installed system.
Persist the swap file in /etc/fstab:
echo '/swap/swapfile none swap defaults,pri=-2 0 0' >> /etc/fstabFind the resume offset, save the integer for Chapter 10:
btrfs inspect-internal map-swapfile -r /swap/swapfile
# Save the integer printed — <RESUME_OFFSET> in Chapter 10.The swap file lives directly on the Arch Btrfs filesystem, so the resume device is that filesystem, addressed by its UUID. We’ll write resume=UUID=<arch-btrfs-uuid> in the GRUB cmdline (Chapter 10), pointing straight at it. The kernel finds the hibernation image from two values: the filesystem UUID (resume=) and the byte offset of the swap file’s first extent (resume_offset=, the integer you just saved).
Chapter 9: zram for everyday compressed swap
cat > /etc/systemd/zram-generator.conf <<'EOF'
[zram0]
zram-size = ram / 2
compression-algorithm = zstd
swap-priority = 100
fs-type = swap
EOFTwo-tier swap arrangement:
| Tier | What | Priority | When the kernel uses it |
|---|---|---|---|
| 1 | /dev/zram0 |
100 | Almost all everyday swap-out, compressed in RAM, no SSD writes. |
| 2 | /swap/swapfile (hibernation) |
-2 | Only on hibernation, or last-resort overflow when zram is exhausted. |
Chapter 10: mkinitcpio and the GRUB cmdline
mkinitcpio
Open the mkinitcpio config in an editor:
nvim /etc/mkinitcpio.confSet these two lines:
MODULES=(btrfs)
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck)
MODULES=(btrfs) makes sure the initramfs can read a Btrfs root. The HOOKS line is the standard systemd-based set; the root mounts directly from the partition at boot. Save, then regenerate the initramfs:
mkinitcpio -PA successful run ends with Image generation successful for each preset.
GRUB cmdline
The cmdline needs two machine-specific values: the Arch Btrfs filesystem UUID and the swap-file resume offset. Print both now so you can paste them straight in without scrolling back to earlier chapters.
<ARCH_BTRFS_UUID> is the Btrfs filesystem UUID of the Arch root — the same one genfstab wrote for /, not a partition UUID. Print it:
blkid -s UUID -o value /dev/nvme0n1p3<RESUME_OFFSET> is the byte offset (in pages) of the swap file’s first extent — the integer you saved back in Chapter 8. Re-print it here so you don’t have to scroll up; the swap file already exists, so this is just a read:
btrfs inspect-internal map-swapfile -r /swap/swapfileNow open the GRUB defaults file in an editor:
nvim /etc/default/grubFind the GRUB_CMDLINE_LINUX_DEFAULT line and set it, pasting in the two values you just printed. Do not include the 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 must add by hand — grub-mkconfig auto-detects the root partition and subvol=@ for you.
Click to expand: every parameter
| Parameter | Purpose |
|---|---|
loglevel=3 |
Quieter kernel boot logs. |
quiet |
Suppress most non-critical kernel boot output. |
zswap.enabled=0 |
Disable zswap; we’re using zram. |
resume=UUID=<ARCH_BTRFS_UUID> |
Resume hibernation from the Arch Btrfs filesystem (where the swap file lives). |
resume_offset=<RESUME_OFFSET> |
Byte offset (in pages) of the swap file’s first extent. |
GRUB’s generated linux line carries root=UUID=… and rootflags=subvol=@ automatically, because grub-mkconfig reads them off the mounted root when it generates the config.
Ubuntu doesn’t exist yet, so for now Arch’s GRUB menu lists only Arch. Once Ubuntu is installed (Part 4), you’ll add a chainload entry for it and regenerate this config from Arch (Part 5) — that’s what produces the single menu listing both OSes.
Install the bootloader and generate the config:
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
grub-mkconfig -o /boot/grub/grub.cfgBoth distros keep /boot on the FAT32 ESP, the standard arrangement GRUB expects, so a plain grub-install is all that’s needed: GRUB reads the kernel and grub.cfg directly off the ESP.
Chapter 11: Enable services, sanity check, reboot
systemctl enable NetworkManager sshd fstrim.timer reflector.timer \
snapper-timeline.timer snapper-cleanup.timer \
grub-btrfsd.serviceFinal sanity check inside the chroot:
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'Expected output shape (your UUIDs and resume offset will differ):
MODULES=(btrfs)
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck)
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 quiet zswap.enabled=0 resume=UUID=<arch-btrfs> resume_offset=1320192"
GRUB_DISABLE_OS_PROBER=false
UUID=<arch-btrfs> / btrfs rw,noatime,compress=zstd:3,subvol=/@ 0 0
UUID=<arch-btrfs> /home btrfs rw,noatime,compress=zstd:3,subvol=/@home 0 0
UUID=<arch-btrfs> /.snapshots btrfs rw,noatime,compress=zstd:3,subvol=/@snapshots 0 0
UUID=<arch-btrfs> /var/log btrfs rw,noatime,compress=zstd:3,subvol=/@var_log 0 0
UUID=<arch-btrfs> /swap btrfs rw,noatime,subvol=/@swap 0 0
UUID=<shared-btrfs> /data btrfs rw,noatime,compress=zstd:3,subvol=/@data,nofail,x-systemd.device-timeout=10s 0 0
/swap/swapfile none swap defaults,pri=-2 0 0
Leave the chroot and reboot:
exit
umount -R /mnt
rebootPull the USB. Expected first-boot sequence:
- Firmware → GRUB menu (just
Arch Linuxfor now). - Kernel + initramfs.
- The root mounts directly and you go straight from GRUB to login.
- systemd starts;
/datamounts;/swap/swapfileactivates; zram comes up. - Login prompt.
If you instead drop into emergency mode, jump to the 🚨 emergency section.
Part 4 — Install Ubuntu 24 LTS alongside
You now have a verified, snapshot-able Arch install with a shared Btrfs data partition. Time to bring up Ubuntu in p4.
Download the Ubuntu 24.04 LTS Desktop ISO, verify its SHA256SUMS and signature, flash to a USB stick, and boot.
Ubuntu’s GUI installer’s manual (“Something else”) mode can format p4 as Btrfs and mount it at /. But it won’t reliably create the @/@home subvolume layout we want, and it likes to reorder things on the ESP. So we install Ubuntu by hand with debootstrap instead: it produces the exact same subvolume layout as the Arch side, uses tooling you already know from Part 1, and never surprises you. If you genuinely don’t care about subvolume layout, the GUI manual mode is a legitimate shortcut — just point it at p4, choose Btrfs and mount point /, and reuse (don’t reformat) the existing ESP at /boot/efi.
BootOrder
When you run Ubuntu’s grub-install, it writes an Ubuntu NVRAM entry and adjusts the boot order so Ubuntu’s GRUB is tried first. We undo that in Part 5 — 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. To compare before/after, dump the current state from the Arch side before booting the Ubuntu USB:
efibootmgr -v > ~/efi-before-ubuntu.txtInstall Ubuntu by hand from the live session
Boot the Ubuntu Desktop USB, choose “Try Ubuntu”, connect to Wi-Fi (debootstrap needs the network), open a terminal, and become root.
Like the Arch install, you can drive this over SSH. Two Ubuntu quirks: the Desktop live image doesn’t ship the SSH server, and you log in as the passwordless ubuntu user. Get the live session online, then:
sudo apt update && sudo apt install -y openssh-server # not on the Desktop live image
sudo passwd ubuntu # the live user has a blank password; SSH rejects that
sudo systemctl enable --now ssh # NB: the unit is 'ssh', not 'sshd'
ip -brief address # note the inet addr on your wlan0 / enpXsY lineThen from your other machine: ssh ubuntu@<ip>, and sudo -i. Root SSH is disabled by default (connect as ubuntu, then sudo), and the live session regenerates a host key every boot, so a “REMOTE HOST IDENTIFICATION HAS CHANGED” warning on reconnect is expected, clear it with ssh-keygen -R <ip>. If apt asks about a modified /etc/ssh/sshd_config, keep the locally installed version.
sudo -i
apt update && apt install -y debootstrap1 — Format p4 as Btrfs and create the subvolumes. We mkfs.btrfs straight onto the partition:
mkfs.btrfs -L ubuntu /dev/nvme0n1p4
mount /dev/nvme0n1p4 /mnt
btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
umount /mnt2 — Mount the target, reusing the existing shared ESP.
mount -o noatime,compress=zstd:3,subvol=@ /dev/nvme0n1p4 /mnt
mkdir -p /mnt/home /mnt/boot/efi
mount -o noatime,compress=zstd:3,subvol=@home /dev/nvme0n1p4 /mnt/home
mount /dev/nvme0n1p1 /mnt/boot/efi # the shared ESP — never reformat it3 — Bootstrap a minimal Ubuntu 24.04 (noble) and write its fstab (including the shared /data line, which on the Ubuntu side is just an fstab entry):
debootstrap --arch amd64 noble /mnt http://archive.ubuntu.com/ubuntu
UB=$(blkid -s UUID -o value /dev/nvme0n1p4)
ESP=$(blkid -s UUID -o value /dev/nvme0n1p1)
SHARED=$(blkid -s UUID -o value /dev/nvme0n1p2)
cat >> /mnt/etc/fstab <<EOF
UUID=$UB / btrfs noatime,compress=zstd:3,subvol=@ 0 0
UUID=$UB /home btrfs noatime,compress=zstd:3,subvol=@home 0 0
UUID=$ESP /boot/efi vfat umask=0077 0 1
UUID=$SHARED /data btrfs rw,noatime,compress=zstd:3,subvol=@data,nofail,x-systemd.device-timeout=10s 0 0
EOF
mkdir -p /mnt/data/data needs no chown from Ubuntu
You already ran chown 1000:1000 /data from Arch in Chapter 7, and ownership is stored in the Btrfs filesystem, not per-OS. Because the Ubuntu user is also UID 1000, Ubuntu sees /data as owned by its user the moment it mounts, no second chown. This is the entire payoff of pinning UID 1000 on both sides.
4 — Enter the 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/bash5 — Inside the chroot: sources, kernel, GRUB, user, desktop. Widen the debootstrap main-only sources list so ubuntu-desktop resolves, then install:
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
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
# locale, timezone, a hostname distinct from Arch's
locale-gen en_US.UTF-8 && update-locale LANG=en_US.UTF-8
ln -sf /usr/share/zoneinfo/<TIMEZONE> /etc/localtime && echo "<HOSTNAME>" > /etc/hostname # e.g. ubuntu
# the UID-1000 user — same NAME and UID as Arch, for /data ownership parity
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 # the graphical layer (skip for a server box)Exit the chroot and reboot. Everything’s written; reboot flushes and unmounts on the way down:
exit
sync
rebootreboot instead of unmounting by hand
The mount --rbind /run /mnt/run we did entering the chroot is recursive, it pulls /run/user/0 and the live session’s snap mount namespaces in under /mnt/run, so a tidy umount -R … often fails with target is busy no matter how many times you retry. The files are already on disk. sync flushes the write cache; reboot tears down every mount the hard way. Pull the Ubuntu USB as the laptop restarts.
Chapter 12: Add the hibernation swap file on Ubuntu
A from-scratch Ubuntu install has no hibernation swap file. Mirror Arch’s Chapter 8 from inside the running Ubuntu system. After the reboot, pick Ubuntu from the GRUB menu and log in:
# Find the Btrfs filesystem mounted at / — should be /dev/nvme0n1p4
findmnt /
# Create the swap subvolume (a manual install doesn't ship one)
sudo btrfs subvolume create /swap
# Make a NoCOW swap file — same size logic as Arch (≥ RAM)
sudo btrfs filesystem mkswapfile --size 36g --uuid clear /swap/swapfile
# Persist in fstab
echo '/swap/swapfile none swap defaults,pri=-2 0 0' | sudo tee -a /etc/fstab
# Find the resume offset
sudo btrfs inspect-internal map-swapfile -r /swap/swapfile
# Save the integer — Ubuntu calls it RESUME_OFFSET too.The cmdline needs two machine-specific values for Ubuntu’s root (p4). The resume offset is the integer you printed just above with map-swapfile; print the Ubuntu Btrfs filesystem UUID too:
sudo blkid -s UUID -o value /dev/nvme0n1p4 # 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 you just printed (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 builder about the hibernation device, then rebuild:
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, and it warns and moves on. What actually performs the resume is the kernel cmdline resume=UUID=… resume_offset=… you set in /etc/default/grub: the kernel resolves the UUID to the device and seeks to the offset. The RESUME file just keeps Ubuntu’s userspace pinned to its own filesystem, never Arch’s.
Don’t trust the absence of the warning as proof; the real test is to actually hibernate (below). The warning also shows up partly because the swap file isn’t active in this just-created session yet, fstab activates it on the next boot.
Set up zram on Ubuntu too
Unlike Fedora, Ubuntu 24.04 does not enable zram out of the box, so a debootstrap install (even with ubuntu-desktop-minimal) comes up with only the hibernation swap file. Mirror Arch’s Chapter 9 to get the same two-tier swap. Ubuntu ships the very same systemd-zram-generator, reading the very same config path:
sudo apt install -y systemd-zram-generator
sudo tee /etc/systemd/zram-generator.conf >/dev/null <<'EOF'
[zram0]
zram-size = ram / 2
compression-algorithm = zstd
swap-priority = 100
fs-type = swap
EOF
sudo systemctl daemon-reload
sudo systemctl start systemd-zram-setup@zram0.service # or just rebootConfirm Ubuntu sees the shared partition and both swap tiers:
findmnt /data # /dev/nvme0n1p2, subvol=@data
swapon --show # zram0 (pri 100) + /swap/swapfile (pri -2)
ls -l /data # files written from Arch should appear, owned by user
sudo systemctl hibernate # screen darkens; power button resumes into UbuntuIf hibernation resumes into a fresh boot instead, re-check RESUME= and re-run sudo update-initramfs -u -k all && sudo update-grub.
Part 6 — Final sanity checks across both OSes
From Arch
findmnt / # subvol=/@, on /dev/nvme0n1p3
findmnt /home # subvol=/@home
findmnt /data # subvol=/@data, on /dev/nvme0n1p2
swapon --show # zram0 (pri 100) + /swap/swapfile (pri -2)
sudo snapper -c root list # at least one snapshot
sudo systemctl hibernate # works, resumes into Arch
ls -l /data # files from Arch + files written from Ubuntu
stat -c '%u %g %n' /data/* # all UIDs/GIDs == 1000From Ubuntu (reboot, pick Ubuntu)
findmnt / # subvol=/@, on /dev/nvme0n1p4
findmnt /home # subvol=/@home
findmnt /data # subvol=/@data, on /dev/nvme0n1p2 (same UUID as Arch sees)
swapon --show # zram + /swap/swapfile (Ubuntu's own)
sudo systemctl hibernate # works, resumes into Ubuntu (separate from Arch's)
ls -l /data # same files as Arch sees, owned by user
stat -c '%u %g %n' /data/* # all UIDs/GIDs == 1000What pass looks like: each OS hibernates and resumes into itself, both mount /data with identical file ownership, both see exactly two swap devices, and the GRUB menu lists both OSes regardless of which side regenerated it last.
For when something has gone visibly wrong. If the Part 6 sanity check passed, skip the rest.
1. Arch boots to a login, but /data doesn’t mount
Thanks to nofail (Chapter 7) you reach a normal login instead of emergency mode. Symptom: findmnt /data prints nothing.
- The UUID in
/etc/fstabdoesn’t match the partition.blkid /dev/nvme0n1p2and compare to the/dataline in/etc/fstab. - The partition didn’t come up in time.
sudo mount /databy hand; if that works, the device was just slow, thex-systemd.device-timeout=10salready keeps boot from hanging. - The
@datasubvolume name is wrong in the mount options. It must besubvol=/@data.
After fixing: sudo systemctl daemon-reload && sudo mount /data && findmnt /data.
2. Boot drops into emergency mode
The usual causes:
- A typo in
/etc/fstab(a bad UUID on a non-nofailline, like/or/home). Boot the Arch USB,mount -o subvol=@ /dev/nvme0n1p3 /mnt,mount /dev/nvme0n1p1 /mnt/boot,arch-chroot /mnt, fix/etc/fstab, reboot. resume=/resume_offset=left as literal<…>text in/etc/default/grub. Same chroot recovery, thengrub-mkconfig -o /boot/grub/grub.cfg.- The initramfs wasn’t regenerated after editing
mkinitcpio.conf. In the chroot:mkinitcpio -P.
4. Hibernation from one OS resumes into the other
Both OSes’ resume= point at the same device. Each OS’s resume= must point at its own Btrfs filesystem UUID, p3 for Arch, p4 for Ubuntu, never the other’s. Fix the cmdline (Arch: /etc/default/grub → grub-mkconfig; Ubuntu: /etc/default/grub + /etc/initramfs-tools/conf.d/resume → update-grub + update-initramfs -u -k all).
5. File ownership on /data looks like 1001 from Ubuntu, 1000 from Arch
The Ubuntu user came out as UID 1001 (you skipped -u 1000, or a UID 1000 already existed). Fix from Ubuntu:
sudo usermod -u 1000 <USERNAME>
sudo groupmod -g 1000 <USERNAME>
sudo find / -uid 1001 -not -path '/proc/*' -not -path '/sys/*' -exec chown 1000 {} +
sudo find / -gid 1001 -not -path '/proc/*' -not -path '/sys/*' -exec chgrp 1000 {} +Reboot Ubuntu. Now id <USERNAME> prints uid=1000 gid=1000.
Part 7 — What comes next
You have two snapshot-able OSes sharing one ESP and one data partition, with hibernation isolated per-OS, and not a single passphrase prompt between power-on and login.
If this machine’s role changes (it becomes a laptop, it starts holding sensitive data), adding encryption means rebuilding on LUKS2: the same subvolume layout and the same shared-/data idea, but with a LUKS2 container wrapped around each root and the data partition. There’s no in-place “add encryption” shortcut on Linux — it means backing up, re-formatting the roots inside LUKS containers, and restoring. Better to decide up front, which is exactly why this article spells out the tradeoff in Part 1.
The Arch desktop layer is a separate, deliberate choice (a custom Hyprland, GNOME, Plasma, sway, whatever) and stops at the console on purpose. Ubuntu comes up graphical out of the box with whichever flavor you installed.
To bolt Windows on, see the plain-Btrfs Triple-Boot Arch + Ubuntu + Windows article, it installs Windows first (so it can’t stomp the ESP), then layers Arch and Ubuntu exactly as above. Windows shares files with Linux through a plain exFAT exchange partition.
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., references here are editorial and follow Canonical’s trademark policy.