Dual-Boot Arch + Ubuntu — With a Shared Data Partition
@, @home, @snapshots, @var_log, @swap). The shared partition is LUKS-encrypted and auto-unlocked at boot via a keyfile stored inside each distro’s already-unlocked root — no double passphrase prompt. Each distro runs zram for everyday compressed swap and hibernates to its own NoCOW Btrfs swap file inside its own encrypted root, so there are no cross-OS resume-image collisions. GRUB on the single shared ESP, with os-prober enabled.
Unlike Article 03 (LUKS+Btrfs single-boot) — which is a first-hand install log I executed and can vouch for command-by-command — this article is the reasoned dual-boot extension of that procedure, not a live-tested install log (yet). It’s built from Article 03’s working stack plus the Arch wiki, the Ubuntu Server / Desktop manual-partitioning docs, and cryptsetup / systemd-cryptsetup-generator man pages.
I’ll be running it for real on the next workstation install and updating in place if anything in the procedure changes. If you spot something wrong, the canonical sources are:
cryptsetup— LUKS container management and keyfile add/removecrypttab—/etc/crypttabsyntax andcrypttab.initramfssemanticsbtrfs— subvolumes, swap files, mount options- Arch wiki: GRUB#Dual booting and
os-prober - Ubuntu wiki: ManualFullSystemEncryption
Treat this as a careful blueprint for the dual-boot pattern, not as a substitute for re-reading the relevant tool docs as you go.
A continuation of Article 03, turning that single-boot Arch install into a dual-boot with Ubuntu 24 LTS sharing the same NVMe drive. Concretely:
- Two independently encrypted root partitions — Arch in its own LUKS2 container, Ubuntu in its own LUKS2 container, each carrying Btrfs subvolumes (
@,@home, …). - One shared 150 GiB Btrfs data partition — also LUKS-encrypted, auto-unlocked at boot from both distros via a keyfile. Mounted at
/dataon Arch and/dataon Ubuntu, owned by your user on both sides. - 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 encrypted root — no shared swap partition, no cross-OS resume-image collisions, no extra LUKS container to maintain.
- One GRUB on the shared ESP, with
os-proberenabled so either distro’sgrub-mkconfigregenerates a working menu listing both.
It stops on the console for the Arch side — desktop layer is your choice, exactly like Article 03 (Caelestia, Article 05 or a custom Hyprland). Ubuntu’s GNOME (or whichever flavor you install) comes up out-of-the-box.
Why this is its own article (and not just “Article 03 with Ubuntu glued on”)
My first attempt at a dual-boot with LUKS, Btrfs, zram, and a shared partition (Article 01) tried to land everything in one swing and didn’t boot. The post-mortem isolated two cryptdevice= typos in the kernel command line and one missing rootflags=subvol=@ parameter. After I fixed those in Article 03, the single-boot version worked end-to-end.
Adding Ubuntu introduces five concerns that single-boot didn’t have:
- Who owns GRUB? Both distros want to install their own bootloader to the same ESP. We pick one as the menu driver (Ubuntu, because its installer wires up
os-proberfor you out of the box) and let it discover Arch automatically. - How does the shared encrypted data partition get unlocked from both sides without prompting twice? A LUKS container can hold up to 8 keyslots. We add the user’s passphrase as slot 0 (for recovery from a live USB), then add a 4096-byte keyfile as slot 1 — and store a copy of that keyfile inside each (already-unlocked) root, referenced from each distro’s
/etc/crypttab. Boot order: root unlock by passphrase → root mounts → crypttab reads keyfile → shared partition unlocks silently. - Resume-image collisions. Two OSes hibernating to the same swap target would overwrite each other’s resume image. We sidestep this entirely by giving each OS its own NoCOW Btrfs swap file inside its own LUKS root, plus zram for everyday compressed swap — exactly the Article 03 pattern, applied twice. There’s no shared swap partition at all: zram handles ~all everyday memory pressure on either OS, and hibernation never has to share state across the boundary.
- UID/GID coordination for the shared partition. If
evannsis 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 into. We pin UID 1000 on both sides during user creation. os-proberis off by default on Arch. Modern Arch GRUB ships withGRUB_DISABLE_OS_PROBER=true. We flip it for both distros so a re-run ofgrub-mkconfigfrom either side regenerates a complete menu.
The result is the install I plan to put on my next workstation when I retire the Dell Precision: Arch for daily-driver work, Ubuntu for the (research-y) packages that don’t have decent Arch coverage, and a single home directory’s worth of documents living in one place — mounted, not synced, not copied.
Part 1 — Bootstrap (same as Article 03)
The first three steps are identical to the basic install and Article 03’s Part 1. In short, from an Arch live USB:
- Verify the ISO with
sha256sum -c sha256sums.txtandgpg --verify. - Boot the USB and confirm UEFI mode with
cat /sys/firmware/efi/fw_platform_size(should print64). - Connect to Wi-Fi via
iwctl, thensystemctl enable --now sshdandpasswdso you can SSH in from a real keyboard. - Keymap, timezone, NTP:
loadkeys us,timedatectl set-timezone America/New_York,timedatectl set-ntp true.
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 the kind of mistake LUKS can’t save you from.
Chapter 1: The four-partition layout
This is the central design decision and the only place the dual-boot story really diverges from Article 03 at the block-device level. Four partitions on one drive:
| Partition | Size | Filesystem | Encryption | Mounted on | Purpose |
|---|---|---|---|---|---|
p1 |
1 GiB | FAT32 | none | /boot |
Shared UEFI ESP (kernels, initramfs, GRUB) |
p2 |
150 GiB | Btrfs (inside LUKS2) | LUKS2 | /data |
Shared data partition, keyfile-unlocked |
p3 |
(remaining ÷ 2) | Btrfs (inside LUKS2) | LUKS2 | / |
Arch root, passphrase-unlocked |
p4 |
(remaining ÷ 2) | Btrfs (inside LUKS2) | LUKS2 | / |
Ubuntu root, passphrase-unlocked (installer) |
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 — there’s no rule that says it has to be 50/50. The rest of this article assumes whatever sizes you pick at cfdisk time; nothing downstream depends on the specific gigabyte counts.
Earlier drafts of this article reserved a 36 GiB shared LUKS swap partition for “everyday overflow swap.” On reflection, it was redundant given the rest of the design:
- zram (compressed RAM swap, set up in Chapter 10) carries ~all everyday memory pressure on either OS. With 16 GiB of zram on 32 GiB RAM, you effectively have ~48–64 GiB of usable memory before anything touches the disk.
- Hibernation writes to a per-OS NoCOW Btrfs swap file inside each encrypted root (Chapter 9 for Arch; Chapter 15 for Ubuntu). That file is sized ≥ RAM and lives where the right OS owns it — no cross-boundary collision.
- A shared swap partition would only kick in for workloads that exceed RAM + zram capacity and aren’t hibernation — a narrow niche.
Dropping it reclaims 36 GiB for the two roots, removes one LUKS container, one keyfile, one crypttab entry per OS, one fstab swap line per OS, and the whole class of failure modes that come with managing another encrypted device. If you specifically know you need a giant overflow swap (very large compile jobs, ML training that exceeds RAM), you can add a swap partition later by shrinking one of the Btrfs roots — but start here and only add complexity if a real workload forces it.
You could legitimately do this differently: one LUKS2 container covering the entire post-ESP drive, then LVM inside it carving out logical volumes for Arch root, Ubuntu root, shared data, and swap. That gives you one passphrase prompt at boot.
| Approach | Pros | Cons |
|---|---|---|
| Two LUKS containers (this article) | Each distro can be reinstalled, resized, or wiped independently. Failure to unlock one root doesn’t gate the other. No LVM layer to learn. | Two passphrase prompts at boot (one per OS, only at install time — once each distro is up, keyfiles unlock everything else silently). |
| One LUKS + LVM inside | Single passphrase prompt at boot. No keyfile dance for shared partitions. Slightly less wasted alignment between LVs. | Forces one distro to own the GRUB-on-LVM-on-LUKS configuration; reinstalling either distro is an LVM-tools exercise; recovery from a corrupt root is harder; rare and harder-to-google failure modes. |
For this article we go with two separate containers. Independence beats the second passphrase prompt — and you only ever type the second prompt when you’re booting that specific OS, not on every boot.
If you’d rather do LUKS+LVM, the dm-crypt / Btrfs / mkinitcpio bits below carry over essentially unchanged; the LVM substitution lives entirely in Chapter 2.
Partition with cfdisk:
cfdisk /dev/nvme0n1Inside cfdisk:
don every existing partition until the table is empty.n→1G→ type EFI System →/dev/nvme0n1p1.n→150G→ type Linux filesystem →/dev/nvme0n1p2(shared data).n→924G→ type Linux filesystem →/dev/nvme0n1p3(Arch root; substitute your half-of-remaining number).n→ rest → type 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 two Arch-side LUKS containers
We format two LUKS containers now (p2 = shared data, p3 = Arch root) and leave p4 for Ubuntu’s installer to format later. The reason: Ubuntu’s “Custom layout” installer can unlock an existing LUKS2 container, but it’s far more reliable to let it format the target partition itself — that way Ubuntu owns the container’s metadata, keyslot policy, and crypttab UUID, with no inherited state from us.
LUKS2 is the modern default in cryptsetup; there is no good reason to force LUKS1 in 2026.
cryptsetup luksFormat --type luks2 /dev/nvme0n1p2 # shared data
cryptsetup luksFormat --type luks2 /dev/nvme0n1p3 # Arch rootEach prompts for YES in capital letters then asks you to type a passphrase twice.
You have two reasonable choices here:
- Two distinct passphrases. Maximum recovery-time clarity (“which one was for which?”). Both go in your password manager. The keyfile mechanism below means you only ever type the Arch root passphrase at boot anyway, so the shared one isn’t a daily burden — it’s rescue-mode-only.
- The same passphrase on both. Less paperwork, slightly worse cryptographic hygiene (a leaked passphrase exposes both containers). Defensible if you store everything in the same password manager anyway.
I’m using option 1 — a Bitwarden entry per container, named LUKS — arch root and LUKS — shared.
There is no recovery if you forget both. Save them somewhere before continuing.
Now unlock both to their canonical mapper names:
cryptsetup open /dev/nvme0n1p2 cryptshared
cryptsetup open /dev/nvme0n1p3 cryptrootThe mapper names matter — they’re the contract between this step, the kernel cmdline, /etc/crypttab, and /etc/fstab. Pick a naming convention and stick with it. I’m using cryptroot for the Arch root (matching Article 03) and cryptshared for the data partition. (Ubuntu’s root will become cryptroot_ubuntu when we install it later, to avoid the name colliding with Arch’s mapper if both are open from a live USB.)
Format the inside of each container:
mkfs.fat -F32 /dev/nvme0n1p1 # ESP — outside LUKS, must stay unencrypted so GRUB can read it
mkfs.btrfs -L shared /dev/mapper/cryptshared
mkfs.btrfs -L arch /dev/mapper/cryptrootmkfs.btrfs against the mapper, not the raw partition — otherwise you’d be putting a Btrfs signature on the raw block device and corrupting the LUKS header.
Chapter 3: Create the Btrfs subvolumes on the Arch root
Same five-subvolume layout as Article 03 — the kernel doesn’t care about the names, but snapper does:
mount /dev/mapper/cryptroot /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 9). |
And one subvolume on the shared data partition — a single @data is enough; you don’t need snapper on data you’ll be backing up to off-machine storage anyway (restic to a NAS, borg to an external drive, whatever your backup tool is). But laying it out as a subvolume from day one lets you add snapshots later if you change your mind, without restructuring:
mount /dev/mapper/cryptshared /mnt
btrfs subvolume create /mnt/@data
umount /mntWhen I say “separate / and /home” what most people mean by that in 2026 is I want a system-rollback to not stomp on my home directory, and I want /home to grow freely without re-partitioning when I run low on space. Both of those are exactly what Btrfs subvolumes give you — they share the underlying Btrfs filesystem’s free space pool (so neither side runs out before the disk does), but they’re independent units for snapshotting, rollback, and quota.
The old-school separate-/home-partition pattern was an ext4-era workaround for ext4’s lack of those features. With Btrfs, separate subvolumes are strictly better: they isolate the same way, share free space, and can be reflinked across.
Chapter 4: Mount everything for pacstrap
Mount the Arch root subvolumes with the right options — identical to Article 03 Chapter 4:
mount -o noatime,compress=zstd:3,subvol=@ /dev/mapper/cryptroot /mnt
mkdir -p /mnt/{boot,home,.snapshots,var/log,swap,data}
mount -o noatime,compress=zstd:3,subvol=@home /dev/mapper/cryptroot /mnt/home
mount -o noatime,compress=zstd:3,subvol=@snapshots /dev/mapper/cryptroot /mnt/.snapshots
mount -o noatime,compress=zstd:3,subvol=@var_log /dev/mapper/cryptroot /mnt/var/log
mount -o noatime,subvol=@swap /dev/mapper/cryptroot /mnt/swap
mount /dev/nvme0n1p1 /mnt/boot
mount -o noatime,compress=zstd:3,subvol=@data /dev/mapper/cryptshared /mnt/dataThe @swap subvolume mounts without compression — a swap file must not be compressed.
Note the extra mount /mnt/data line vs. Article 03. That’s the shared partition, mounted now so that genfstab -U /mnt in the next chapter captures it automatically (so we get the fstab line for free, and only have to add the crypttab entry by hand).
Chapter 5: Pacstrap, fstab, chroot
Same shape as Article 03’s Chapter 5, with no new packages — the dual-boot-specific bits are configuration, not new 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 \
cryptsetup \
btrfs-progs \
grub efibootmgr grub-btrfs os-prober \
snapper snap-pac \
networkmanager openssh sudo \
neovim git base-devel \
zram-generator \
inotify-tools \
reflectorUse amd-ucode instead of intel-ucode on AMD hardware. The one package added vs. Article 03 is os-prober — the script GRUB runs to discover other operating systems on the same disk. Without it, grub-mkconfig would only ever see Arch and the menu would have a single entry.
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 (not the LUKS partition’s UUID), 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 — that’s correct. If you see only one Btrfs UUID, you skipped a mount somewhere; re-run Chapter 4.
Part 2 — Inside the chroot
Chapter 6: Locale, time, hostname, user — with a pinned UID
Identical to Article 03’s Chapter 6 except for one dual-boot-specific change: we pin the user’s UID and GID to 1000 explicitly. Ubuntu’s installer assigns the first regular user UID 1000 by default, so if we also pin to 1000 here, files on /data will be owned by the same user-as-the-kernel-sees-it on both sides.
# 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/America/New_York /etc/localtime
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 "arch-dual" > /etc/hostname
# /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 evanns
useradd -m -u 1000 -g 1000 -G wheel,video,audio,input evanns
passwd evanns
# sudo for wheel
sed -i 's/^# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) ALL/' /etc/sudoersReplace evanns 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 evanns is UID 1000 on Arch and evanns is UID 1001 on Ubuntu, the file ownership on /data literally cannot match in both directions — somebody’s ls -l /data will show 1001 instead of evanns. 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 evanns && groupmod -g 1000 evanns && find / -uid 1001 -exec chown 1000 {} + — workable but tedious. Pin in advance.
Chapter 9: Hibernation swap file for Arch (per Article 03)
Identical to Article 03 Chapter 7. zram (Chapter 10) 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 12).
swapon from inside the chroot
Same warning as Article 03 — 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 0 0' >> /etc/fstabzram at priority 100 (Chapter 10) will be used first; the hibernation swap file (default priority, ~-2) only kicks in under heavy memory pressure or when systemd writes the hibernation image.
Find the resume offset:
btrfs inspect-internal map-swapfile -r /swap/swapfileSave the integer printed — <RESUME_OFFSET> in Chapter 12.
Chapter 10: zram for everyday compressed swap
Same as Article 03 Chapter 8:
cat > /etc/systemd/zram-generator.conf <<'EOF'
[zram0]
zram-size = ram / 2
compression-algorithm = zstd
swap-priority = 100
fs-type = swap
EOFNote the two-tier swap arrangement we end up with:
| Tier | What | Priority | When the kernel uses it |
|---|---|---|---|
| 1 | /dev/zram0 |
100 | Almost all everyday swap-out lands here — compressed in RAM, no SSD writes. |
| 2 | /swap/swapfile (hibernation) |
-2 | Only on actual hibernation, or last-resort overflow when zram is exhausted. |
That’s it — two tiers, no third “shared swap partition” middle tier. If you ever discover a workload that genuinely needs more than RAM + zram worth of working memory and isn’t a hibernation case, you can carve out a swap partition later (or grow the swap file inside @swap). Don’t pre-pay for it.
Chapter 11: mkinitcpio — same as Article 03
Open /etc/mkinitcpio.conf and set:
MODULES=(btrfs)
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt filesystems fsck)
The reasoning is identical to Article 03 Chapter 9 — only the Arch root container needs to be unlockable from the initramfs, and sd-encrypt + rd.luks.name=… is the path that does it.
Add the root entry to /etc/crypttab.initramfs (it gets copied into the initramfs at build time and makes the configuration discoverable post-install):
echo "cryptroot UUID=$(blkid -s UUID -o value /dev/nvme0n1p3) none luks" \
> /etc/crypttab.initramfsRegenerate the initramfs:
mkinitcpio -PA successful run ends with Image generation successful for each preset (linux, plus linux-lts if installed).
crypttab.initramfs
The initramfs’s job is to unlock just enough to mount / — and that’s only cryptroot. The shared data partition is unlocked by the running system’s systemd-cryptsetup unit, generated from /etc/crypttab (the non-.initramfs one). It’d fail to unlock in the initramfs anyway because the keyfile path /etc/cryptsetup-keys.d/shared.key doesn’t exist until / is mounted.
Chapter 12: GRUB cmdline
Get the Arch root LUKS partition’s UUID (/dev/nvme0n1p3, not the mapper):
blkid -s UUID -o value /dev/nvme0n1p3Open /etc/default/grub and replace the GRUB_CMDLINE_LINUX_DEFAULT line with:
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 quiet zswap.enabled=0 rd.luks.name=<UUID>=cryptroot root=/dev/mapper/cryptroot rootflags=subvol=@ resume=/dev/mapper/cryptroot resume_offset=<RESUME_OFFSET>"Replace <UUID> with the Arch root LUKS UUID and <RESUME_OFFSET> with the integer from Chapter 9. Do not include the angle brackets in the actual file — Article 01’s failure mode 1, see emergency section.
Same line-by-line breakdown as Article 03:
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. |
rd.luks.name=<UUID>=cryptroot |
Unlock LUKS partition <UUID> and expose it at /dev/mapper/cryptroot. |
root=/dev/mapper/cryptroot |
The unlocked mapper is the root device. |
rootflags=subvol=@ |
Mount the @ Btrfs subvolume as /. |
resume=/dev/mapper/cryptroot |
Resume hibernation from this device (the LUKS-unlocked Btrfs). |
resume_offset=<RESUME_OFFSET> |
Byte offset (in pages) of the swap file’s first extent. |
Now enable os-prober — the key dual-boot setting. In /etc/default/grub, find the commented line #GRUB_DISABLE_OS_PROBER=false and uncomment it:
sed -i 's/^#GRUB_DISABLE_OS_PROBER=false/GRUB_DISABLE_OS_PROBER=false/' /etc/default/grubVerify:
grep '^GRUB_DISABLE_OS_PROBER=' /etc/default/grub
# Should print: GRUB_DISABLE_OS_PROBER=falseRight now (Arch’s pre-Ubuntu state) os-prober will find nothing — Ubuntu isn’t installed yet. But we want the flag flipped so when we re-run grub-mkconfig from Arch after Ubuntu is installed, the menu picks up Ubuntu automatically.
Install the bootloader and generate the GRUB config:
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
grub-mkconfig -o /boot/grub/grub.cfgThe grub-install --bootloader-id=GRUB writes a GRUB entry to the EFI NVRAM and copies the GRUB binaries to /boot/EFI/GRUB/. When Ubuntu’s installer runs grub-install later, it writes a separate Ubuntu NVRAM entry and copies its own binaries to /boot/EFI/Ubuntu/. The ESP holds both sets simultaneously — firmware decides which to chainload based on NVRAM boot-order.
GRUB_ENABLE_CRYPTODISK=y
Same reason as Article 03 — /boot is the unencrypted FAT32 ESP, so GRUB never reads encrypted blocks itself. Leaving GRUB_ENABLE_CRYPTODISK unset is correct.
Chapter 13: 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 — same shape as Article 03:
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|cryptshared'
cat /etc/crypttab | grep -v '^#'
ls -l /etc/cryptsetup-keys.d/Expected output shape (your UUIDs and resume offset will differ):
MODULES=(btrfs)
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt filesystems fsck)
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 quiet zswap.enabled=0 rd.luks.name=<arch-luks-uuid>=cryptroot root=/dev/mapper/cryptroot rootflags=subvol=@ resume=/dev/mapper/cryptroot 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 0 0
/swap/swapfile none swap defaults 0 0
cryptshared UUID=<shared-luks-uuid> /etc/cryptsetup-keys.d/shared.key luks
-r-------- 1 root root 4096 May 19 14:23 shared.key
Then leave the chroot and reboot, in this exact order:
exit
umount -R /mnt
cryptsetup close cryptroot
cryptsetup close cryptshared
rebootPull the USB. Expected first-boot sequence:
- Firmware → GRUB menu (just
GRUB/Arch Linuxfor now — Ubuntu isn’t installed yet). - Kernel + initramfs.
sd-encryptprompts:Please enter passphrase for disk arch (cryptroot):— type the Arch root passphrase.@subvolume mounts as/.- systemd starts;
systemd-cryptsetup@cryptshared.servicereads the keyfile and unlocks the shared container silently. /datamounts;/swap/swapfileactivates; zram device 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, encrypted, snapshot-able Arch install with one shared LUKS data partition sitting unused-by-Ubuntu-yet. Time to bring up Ubuntu in p4 (the empty partition we left for it in Chapter 1).
Download the Ubuntu 24.04 LTS Desktop ISO (or Server, if you’d rather configure a desktop later — same procedure either way), verify its SHA256SUMS and signature, flash to a USB stick, and boot.
BootOrder
When Ubuntu’s installer runs grub-install, it writes an Ubuntu entry to NVRAM and adjusts the boot order so Ubuntu’s GRUB is tried first. That’s fine — Ubuntu’s GRUB, with os-prober enabled, picks up Arch automatically and shows both in its menu. But if you’d rather Arch’s GRUB own the menu, you’ll restore the order with efibootmgr in Part 6.
If you want to be safe before continuing, dump the current NVRAM state so you can compare later:
efibootmgr -v > ~/efi-before-ubuntu.txtRun that from the Arch side, before booting the Ubuntu installer.
In the Ubuntu installer
- Pick “Install Ubuntu” (not “Try Ubuntu without installing”, though either gets you to the installer eventually).
- Keyboard, network, updates — answer normally. Network: connect to the same Wi-Fi as before so
/dev/disk/by-uuidmappings the live system sees match what the installed system will see. - Installation type → “Something else” (the manual-partitioning option — the other choices will try to wipe the whole disk).
- In the partition table editor:
/dev/nvme0n1p1— select it, “Change”, set “Use as: EFI System Partition” → DO NOT FORMAT (the checkbox at the bottom — leave it unchecked). This shares Arch’s ESP with Ubuntu./dev/nvme0n1p2— leave it alone (Ubuntu will recognize it as a LUKS container holding the shared/data; don’t let the installer touch it)./dev/nvme0n1p3— same, leave alone (this is Arch’s root; Ubuntu must not even read it)./dev/nvme0n1p4— select it, “Change”, set “Use as: physical volume for encryption”, format checked. The installer will prompt for a LUKS passphrase. Use a distinct passphrase from Arch’s root — save it in your password manager asLUKS — ubuntu root.- After confirming the encryption setup, the partition will show as a mapper (e.g.,
/dev/mapper/nvme0n1p4_crypt). Select that mapper, “Change”, set “Use as: btrfs journaling file system”, “Mount point: /”, format checked.
- “Device for boot loader installation” — set this to
/dev/nvme0n1(the disk, not a partition). Ubuntu’sgrub-installwill write to the ESP we created. - User account creation: username
evanns(match exactly what you used on Arch), tick “Log in automatically” off, password whatever. The installer does not expose the UID for the first user — it’s hard-coded to 1000, which is exactly what we want. - Confirm, install.
The Something else flow gives you a partition table editor that drives parted, cryptsetup, mkfs.btrfs, and grub-install for you. Equivalent to roughly:
cryptsetup luksFormat --type luks2 /dev/nvme0n1p4
cryptsetup open /dev/nvme0n1p4 nvme0n1p4_crypt
mkfs.btrfs -L ubuntu /dev/mapper/nvme0n1p4_crypt
# … debootstrap of Ubuntu rootfs into a Btrfs filesystem with @ and @home subvolumes …
mount /dev/nvme0n1p1 /target/boot/efi
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu /dev/nvme0n1
update-initramfs -u
update-grub # Ubuntu's wrapper around grub-mkconfigupdate-grub with os-prober enabled (which is the Ubuntu default) discovers Arch on /dev/nvme0n1p3: it sees the LUKS container exists and adds an “Arch Linux GNU/Linux” entry that chainloads our /boot/EFI/GRUB/grubx64.efi. Functionally: at the GRUB menu after Ubuntu install, picking “Ubuntu” boots Ubuntu directly; picking “Arch Linux” chains to Arch’s GRUB and from there to Arch.
- When the installer finishes, don’t reboot yet — instead pick “Continue Testing”, open a terminal in the live session, and continue with Chapter 14 below.
Chapter 15: Add the hibernation swap file on Ubuntu
Ubuntu’s installer doesn’t create a hibernation swap file by default. We mirror Arch’s Chapter 9 from inside the running Ubuntu system.
After the reboot, pick Ubuntu from the GRUB menu (or Arch Linux to flip back if you want to compare; we’ll boot Ubuntu first to finish its config).
Log in as evanns. Open a terminal:
# Find the Btrfs filesystem mounted at / — should be /dev/mapper/nvme0n1p4_crypt
findmnt /
# Create the swap subvolume (Ubuntu's installer 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 for the cmdline
sudo btrfs inspect-internal map-swapfile -r /swap/swapfile
# Save the integer — Ubuntu calls it RESUME_OFFSET too.Edit /etc/default/grub. Ubuntu’s GRUB_CMDLINE_LINUX_DEFAULT typically reads quiet splash. Add the resume= and resume_offset= parameters, using the Ubuntu root LUKS partition’s UUID (not Arch’s), the mapper name Ubuntu uses (nvme0n1p4_crypt), and the offset from the previous step:
sudo blkid -s UUID -o value /dev/nvme0n1p4 # save this as <UBUNTU_LUKS_UUID>
sudoedit /etc/default/grubSet:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash resume=/dev/mapper/nvme0n1p4_crypt resume_offset=<UBUNTU_RESUME_OFFSET>"
Tell Ubuntu’s initramfs builder about the hibernation device:
echo "RESUME=/dev/mapper/nvme0n1p4_crypt" | sudo tee /etc/initramfs-tools/conf.d/resume
sudo update-initramfs -u -k all
sudo update-grub/etc/initramfs-tools/conf.d/resume file
On Ubuntu, initramfs-tools (the analog of Arch’s mkinitcpio) reads /etc/initramfs-tools/conf.d/resume at build time and bakes the resume device into the initramfs so userspace’s hibernation tooling agrees with the kernel cmdline. Without this file, update-initramfs may pick an unintended device. Pinning to the per-OS Btrfs swap file’s underlying mapper is what guarantees Ubuntu always resumes from its own hibernation image, never Arch’s.
Confirm Ubuntu sees the shared partition and its own swap correctly:
findmnt /data # /dev/mapper/cryptshared, subvol=@data
swapon --show # zram (recent Ubuntu sets it up automatically) + /swap/swapfile
cryptsetup status cryptshared
ls -l /data # files written from Arch should appear, owned by evannsTest hibernation:
sudo systemctl hibernateScreen goes dark, fans stop. Press the power button to resume. You should see: GRUB menu → kernel/initramfs → Ubuntu LUKS passphrase prompt → resume completes, exact same desktop state as before.
If hibernation resumes into a fresh boot instead, re-check RESUME= in /etc/initramfs-tools/conf.d/resume and re-run sudo update-initramfs -u -k all && sudo update-grub.
Part 6 — Final sanity checks across both OSes
Before treating the dual-boot as “done”:
From Arch
findmnt / # subvol=/@, on cryptroot
findmnt /home # subvol=/@home
findmnt /data # subvol=/@data, on cryptshared
swapon --show # zram0 (pri 100) + /swap/swapfile (pri -2)
cryptsetup status cryptroot # active LUKS2
cryptsetup status cryptshared # active LUKS2
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 nvme0n1p4_crypt
findmnt /home # subvol=/@home
findmnt /data # subvol=/@data, on cryptshared (same UUID as Arch sees)
swapon --show # zram + /swap/swapfile (Ubuntu's own, distinct from Arch's)
cryptsetup status nvme0n1p4_crypt
cryptsetup status cryptshared
sudo systemctl hibernate # works, resumes into Ubuntu (separate from Arch's hibernation)
ls -l /data # same files as Arch sees, owned by evanns
stat -c '%u %g %n' /data/* # all UIDs/GIDs == 1000What pass looks like at the system level: each OS hibernates and resumes into itself without touching the other, both mount /data with identical file ownership, both see exactly two swap devices (zram + their own per-OS swap file), and the GRUB menu lists both OSes regardless of which grub-mkconfig/update-grub regenerated it last.
If any of these fail, the emergency section below covers the common dual-boot-specific failure modes.
This section is for when something has gone visibly wrong. If the Part 6 sanity check passed, you can skip the rest of this article.
1. Arch boots, but /data doesn’t mount on first boot
Symptom: findmnt /data prints nothing. journalctl -b -u systemd-cryptsetup@cryptshared.service shows Failed to activate with key file ….
Most-likely causes:
- The keyfile path in
/etc/crypttabdoesn’t match where the keyfile actually lives.cat /etc/crypttabandls -l /etc/cryptsetup-keys.d/— they have to match. - The keyfile’s permissions are wrong (must be
0400, root-owned).chmod 0400 /etc/cryptsetup-keys.d/shared.key. - The keyfile was never added to the LUKS container’s keyslot.
cryptsetup luksDump /dev/nvme0n1p2 | grep -i keyslotshould show two active keyslots; if only one, re-runcryptsetup luksAddKey /dev/nvme0n1p2 /etc/cryptsetup-keys.d/shared.key. - The UUID in
/etc/crypttabdoesn’t match the partition’s actual LUKS UUID.blkid /dev/nvme0n1p2— compare.
2. Boot drops into emergency mode after Arch LUKS passphrase
Same four failure modes as Article 03’s emergency section:
<UUID>or<RESUME_OFFSET>left as literal text in/etc/default/grub.- Mapper name mismatch between
rd.luks.name=…=cryptrootandroot=/dev/mapper/cryptroot. rootflags=subvol=@missing.mkinitcpio.confnot regenerated after edits (nosd-encrypthook in the actual image).
Recover the same way: boot the Arch USB, cryptsetup open /dev/nvme0n1p3 cryptroot, mount -o subvol=@ /dev/mapper/cryptroot /mnt, mount /dev/nvme0n1p1 /mnt/boot, arch-chroot /mnt, fix, mkinitcpio -P, grub-mkconfig -o /boot/grub/grub.cfg, reboot.
4. Hibernation from one OS resumes into the other OS
This means both OSes’ resume= parameters point at the same device, and whichever boots last reads the other’s image. The fix: each OS’s resume= parameter should point at its own per-OS Btrfs swap file’s underlying mapper — /dev/mapper/cryptroot for Arch, /dev/mapper/nvme0n1p4_crypt for Ubuntu — and never at the other OS’s mapper.
Fix the cmdline (Arch: /etc/default/grub → grub-mkconfig; Ubuntu: /etc/default/grub → update-grub; Ubuntu also: /etc/initramfs-tools/conf.d/resume → update-initramfs -u -k all).
5. File ownership on /data looks like 1001 from Ubuntu, 1000 from Arch
The Ubuntu installer assigned the user UID 1001 because there was already a UID 1000 in some inherited config (rare, but possible). Fix from Ubuntu:
sudo usermod -u 1000 evanns
sudo groupmod -g 1000 evanns
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 evanns prints uid=1000(evanns) gid=1000(evanns).
6. Ubuntu installer can’t see the Arch ESP
Symptom: when you select /dev/nvme0n1p1 in the installer, “Use as: EFI System Partition” is greyed out, or the installer wants to format it.
Workaround: pre-mount it from the live ISO’s terminal before re-launching the installer:
sudo mount /dev/nvme0n1p1 /mnt
ls /mnt/EFI # should see GRUB/ from Arch's grub-install
sudo umount /mntThen re-launch the installer — the partition type recognition usually settles after a clean mount-cycle.
Part 7 — What comes next
You now have two encrypted, snapshot-able OSes sharing one ESP, one swap partition, and one data partition, with hibernation isolated per-OS. The next decisions are personal:
Caelestia (Article 05) for a turnkey Hyprland bundle, or a custom Hyprland (upcoming). Neither affects Ubuntu — desktop choice lives entirely on the Arch side.
Comes up out-of-the-box with whichever Ubuntu flavor you installed (GNOME for stock Ubuntu Desktop, Plasma for Kubuntu, etc.). No further configuration required to log in graphically.
If you ever want to bolt Windows onto this layout, that’s Article 10 territory — the dual-boot above doesn’t preclude it, but Windows adds BitLocker, signed-bootloader, and ESP-conflict considerations that deserve their own article.
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.