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.
An evolution based on 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 (a custom Hyprland, GNOME, Plasma, or whatever you prefer). 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
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 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 the Arch live environment
The Arch side of this install begins exactly like the basic install and Article 03: verify the ISO, boot the live USB, get online, set the clock. Earlier drafts of this article just said “do the first three steps like Article 03” and moved on — but a dual-boot is exactly the kind of multi-hour job where you don’t want to be flipping between three browser tabs to remember how gpg --verify goes. So this part is spelled out in full. If you’ve done a from-scratch Arch install before, skim to Chapter 1.
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, sd-encrypt) 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 "Your SSID Here"
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>openssh— the package shipping both client and server (already installed on the live ISO).ssh— the client you run to connect out.sshd— the server daemon that must be enabled before you can connect in.
You’re enabling sshd here so the live USB accepts an incoming connection from your other machine.
Keymap, timezone, time sync
loadkeys us # your console keymap
timedatectl set-timezone America/New_York # substitute your zone
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 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 crypt_shared
cryptsetup open /dev/nvme0n1p3 crypt_archThe 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 crypt_arch for the Arch root and crypt_shared for the data partition (Article 03 calls the Arch root cryptroot; here it becomes crypt_arch so the name itself says which OS’s root it is, now that more than one is in play). Ubuntu’s root is the third piece of the scheme — conceptually crypt_ubuntu — but you don’t get to pick it the same way: Ubuntu’s guided installer auto-names the mapper after its partition, typically nvme0n1p4_crypt, and that’s the name that ends up in Ubuntu’s own /etc/crypttab and resume= line. We use that installer name verbatim later in the article rather than fight it.
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/crypt_shared
mkfs.btrfs -L arch /dev/mapper/crypt_archmkfs.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/crypt_arch /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/crypt_shared /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/crypt_arch /mnt
mkdir -p /mnt/{boot,home,.snapshots,var/log,swap,data}
mount -o noatime,compress=zstd:3,subvol=@home /dev/mapper/crypt_arch /mnt/home
mount -o noatime,compress=zstd:3,subvol=@snapshots /dev/mapper/crypt_arch /mnt/.snapshots
mount -o noatime,compress=zstd:3,subvol=@var_log /dev/mapper/crypt_arch /mnt/var/log
mount -o noatime,subvol=@swap /dev/mapper/crypt_arch /mnt/swap
mount /dev/nvme0n1p1 /mnt/boot
mount -o noatime,compress=zstd:3,subvol=@data /dev/mapper/crypt_shared /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 user
useradd -m -u 1000 -g 1000 -G wheel,video,audio,input user
passwd user
# sudo for wheel
sed -i 's/^# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) ALL/' /etc/sudoersReplace user 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 user 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 user. 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 user && groupmod -g 1000 user && 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 "crypt_arch 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 crypt_arch. 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>=crypt_arch root=/dev/mapper/crypt_arch rootflags=subvol=@ resume=/dev/mapper/crypt_arch 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>=crypt_arch |
Unlock LUKS partition <UUID> and expose it at /dev/mapper/crypt_arch. |
root=/dev/mapper/crypt_arch |
The unlocked mapper is the root device. |
rootflags=subvol=@ |
Mount the @ Btrfs subvolume as /. |
resume=/dev/mapper/crypt_arch |
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|crypt_shared'
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>=crypt_arch root=/dev/mapper/crypt_arch rootflags=subvol=@ resume=/dev/mapper/crypt_arch 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 0 0
crypt_shared UUID=<shared-luks-uuid> /etc/cryptsetup-keys.d/shared.key luks,nofail
-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 crypt_arch
cryptsetup close crypt_shared
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 (crypt_arch):— type the Arch root passphrase.@subvolume mounts as/.- systemd starts;
systemd-cryptsetup@crypt_shared.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
user(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 user. 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/crypt_shared, subvol=@data
swapon --show # zram (recent Ubuntu sets it up automatically) + /swap/swapfile
sudo cryptsetup status crypt_shared
ls -l /data # files written from Arch should appear, owned by userTest 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 crypt_arch
findmnt /home # subvol=/@home
findmnt /data # subvol=/@data, on crypt_shared
swapon --show # zram0 (pri 100) + /swap/swapfile (pri -2)
sudo cryptsetup status crypt_arch # active LUKS2
sudo cryptsetup status crypt_shared # 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 crypt_shared (same UUID as Arch sees)
swapon --show # zram + /swap/swapfile (Ubuntu's own, distinct from Arch's)
sudo cryptsetup status nvme0n1p4_crypt
sudo cryptsetup status crypt_shared
sudo systemctl hibernate # works, resumes into Ubuntu (separate from Arch's hibernation)
ls -l /data # same files as Arch sees, owned by user
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 to a login, but /data doesn’t mount
Thanks to nofail (Chapter 8) this is the good version of the failure: you reach a normal login instead of emergency mode, and you debug from a comfortable shell. Symptom: findmnt /data prints nothing, and journalctl -b -u systemd-cryptsetup@crypt_shared.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 -A30 '^Keyslots:'should show two active keyslots (0:and1:); 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.
After fixing the cause, retry without rebooting:
sudo systemctl daemon-reload # re-read /etc/crypttab and /etc/fstab
sudo systemctl restart 'systemd-cryptsetup@crypt_shared.service'
sudo mount /data # or: sudo systemctl restart data.mount
findmnt /data # should now show the shared Btrfs filesystem2. 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=…=crypt_archandroot=/dev/mapper/crypt_arch. 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 crypt_arch, mount -o subvol=@ /dev/mapper/crypt_arch /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/crypt_arch 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 user
sudo groupmod -g 1000 user
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 user prints uid=1000(user) gid=1000(user).
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:
The desktop layer is a separate, deliberate choice that lives entirely on the Arch side and doesn’t affect Ubuntu: a custom Hyprland built piece by piece (upcoming), or any environment you like (GNOME, Plasma, sway, dwm). Stock Arch stops at the console on purpose.
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 — Triple-Boot Arch + Ubuntu + Windows territory — it builds directly on this dual-boot, installing Windows first (so it can’t stomp the ESP), then layering Arch and Ubuntu exactly as above. The one design change: Windows stays out of the encrypted shared /data entirely (it can’t read LUKS+Btrfs), so cross-OS exchange with Windows uses a separate plain 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.