Rebuilding a Work Laptop on LUKS+Btrfs: Everything That Can Go Wrong During an 'Ordinary' Fresh Install
"Fresh install" sounds like an hour-long weekend chore. The reality with LUKS+Btrfs+hibernation on a laptop with hybrid graphics was a bit different — this is a diary of what actually happened.
The decision: why LUKS and why Btrfs
The goal wasn't just to reinstall the system, but to add two things that had been missing: full disk encryption (LUKS) and a snapshot-capable filesystem (Btrfs) as insurance against a bad update or experiment. LUKS was a deliberate extra choice — standard security practice worth genuinely practicing, not just skimming past.
The first step was mundane but important: deciding which physical disk the system would go on. The plan flipped once during preparation — the faster disk (PCIe4) had originally been set aside as a separate Btrfs lab disk for a future VM/pentest lab, but since that lab wasn't urgent yet, it made more sense to put the main system on it directly. The old disk stayed as a live safety net until the new system was confirmed working — a safer order than wiping something you can't get back the moment you start.
The first boot: BusyBox instead of a system
The install went through without issues. Reboot — and instead of a login screen, a black console with an (initramfs) prompt. No error message, just silence.
Diagnosis happened from the outside — from the still-working old system on the second disk, over SSH, mounting the new disk as secondary. The cause: /etc/crypttab on the new system was completely empty. The installer had correctly written /etc/fstab and the boot loader entry, but never wrote down how the LUKS partition was supposed to unlock at boot — a consequence of the LUKS partition having been prepared by hand beforehand, rather than through the installer's fully automated flow, which evidently skips this step when it takes over an existing LUKS partition.
The fix (manually opening LUKS, adding the crypttab line, chroot, repacking the initramfs) looked complete. Second reboot — BusyBox again.
The second failure: same error, different cause
This is a moment worth calling out on its own: the same symptom doesn't have to have the same cause the second time around. In the first fix, the diagnostic LUKS mapper had been left named differently (newroot_check) from what /etc/crypttab expected (cryptroot). The script printed a name-mismatch warning while repacking the initramfs — a warning that looked like cosmetic noise, but was actually naming the exact problem: the initramfs-tools cryptsetup hook figures out which crypttab entry belongs to the root disk based on the currently attached mapper name.
With the mapper correctly named exactly as it appeared in crypttab, the warning disappeared entirely. The third reboot finally went through — LUKS password, clean boot, system running.
A side lesson from this phase worth remembering on any machine with multiple NVMe drives: the numbering of /dev/nvme0n1 vs. nvme1n1 can change between reboots of the same machine, not just between a live USB and the installed system. The only reliable way to identify a disk is its PARTUUID, never its ordinal number.
Timeshift, silently doing nothing
A day later came another, quieter bug: Timeshift had daily snapshots configured, but after several days there wasn't a single one. The cron job ran on schedule, but with MAILTO="" — so every silent failure stayed genuinely silent. The real error: The system partition has an unsupported subvolume layout. Only ubuntu-type layouts with @ and @home subvolumes are currently supported.
The graphical installer can't directly assign named Btrfs subvolumes for root during custom partitioning — root had ended up as a bare top-level subvolume, while /home was a proper @home subvolume. Timeshift's Btrfs mode requires exactly the @/@home pair, named precisely.
Fixing it required physically rebooting into the second (old) disk as a safety net and reworking the subvolume structure for real. Along the way, reality turned out to be slightly different from what the plan assumed — the @ subvolume already existed, just empty and unused, so it was enough to move the content into it and fix fstab, rather than creating a new subvolume from scratch.
When moving data breaks something completely unrelated
Moving content across a subvolume boundary (mv from top-level into @) is, on Btrfs, an actual data copy, not a fast rename. That had an unexpected side effect: the swap file for hibernation (set up only a few hours earlier, with a deliberate chattr +C/no-COW attribute needed for swap to work on Btrfs at all) lost that attribute and fragmented from one contiguous extent into 27 pieces. Hibernation, which had just been set up as the primary sleep mechanism, would have silently stopped working because of it.
The swapfile had to be deleted and recreated the proper way (truncate → chattr +C → fallocate → mkswap), and resume_offset recalculated from its new physical location.
Right after that came another, separate Timeshift problem: cannot snapshot '@': source subvolume contains an active swapfile. Btrfs can't atomically snapshot a subvolume that has an active swap file living inside it. The fix: move the swap into its own nested subvolume (/swap), which gets automatically skipped when snapshotting the parent @ — nested subvolumes show up inside a parent's snapshot as an empty directory.
Hibernation that almost worked — and the wrong tool for computing one number
With Timeshift fixed, it was time for a real hibernation test. It failed immediately: Failed to find location to hibernate to: Invalid argument. The running kernel still had the old resume_offset value from the last boot — a change in the boot loader only takes effect after a reboot, not live.
After setting it manually and live, a second, more subtle error appeared: Cannot find swap entry corresponding to /sys/power/resume. The resume_offset value, computed with filefrag -v, didn't match what systemd-logind was internally expecting for the actual swap file.
This is a detail worth its own sentence: filefrag -v gives the wrong resume-offset value on Btrfs, because it differs from the real physical position due to how Btrfs internally translates logical and physical data placement (the chunk-tree translation layer). The right tool is btrfs inspect-internal map-swapfile -r <path> — it gave a completely different value, and that one was correct. It's entirely possible the very first offset calculation from earlier that morning was wrong in the exact same way from the start, and simply never got tested for real before the next migration step overwrote it anyway.
With the correct value, hibernation finally worked — and Timeshift and hibernation ran side by side without stepping on each other.
Graphics that wake up to a black screen
A separate, parallel problem: after waking from sleep (or from hibernation), the screen would occasionally stay black with a blinking cursor, even though the system kept running underneath (SSH still worked). Cause: a DRM "master handoff" conflict between the Intel and NVIDIA graphics after resume, triggered by an i915 link-training failure while re-establishing the display signal.
Two direct fix attempts (disabling PSR, a newer kernel) didn't resolve it — the error repeated identically. Investigation showed this is a well-documented upstream bug that mostly shows up on desktop environments outside GNOME (COSMIC, which this laptop uses, is one of them) — GNOME/mutter evidently has more robust recovery from this specific failure.
Instead of chasing the exact cause further inside an alpha-stage cosmic-comp, the pragmatic decision was to switch lid-close behavior from suspend (s2idle) to hibernation, which never experiences this particular suspend/resume race at all — a workaround rather than a fix, but a reliable one.
Wi-Fi that "froze" — and it was neither graphics nor disk
The last piece of the puzzle looked completely unrelated at first: the system would occasionally stop responding to clicks and then suddenly open a pile of windows at once, as if the clicks had been buffered up. Digging through the logs revealed the real cause: the Wi-Fi card had power-save mode enabled, and its firmware kept crashing (dozens of times a day) whenever it transitioned into or out of it. A few-seconds-to-minutes Wi-Fi outage caused a network-mounted drive (a CIFS mount to the home server) to freeze for tens of seconds at a time — which is exactly what looked, from the outside, like "the system froze."
The fix was simple, it just took finding the right layer first: disable Wi-Fi power-save permanently through the NetworkManager configuration.
What to take away from this migration
- The same symptom can have a different cause the second time. BusyBox after the first fix and after the second fix looked identical — the causes were completely different.
- A "harmless" warning is often not harmless. The mapper name mismatch looked like cosmetic noise until it turned out to name the actual bug precisely.
- A low-level filesystem change can break something that looks completely unrelated at first glance. Moving data across a subvolume boundary silently destroyed properties the hibernation swap file needed to function.
- Use the tool that's actually meant for that filesystem, not the one that "usually works."
filefragis reliable on ext4; on Btrfs it gives misleading numbers for exactly this purpose. - A symptom can only be diagnosed once you know what actually changed. A freezing system that looked like a graphics or disk problem turned out, in the end, to be the Wi-Fi card.
- Having a working safety net (a second disk with the old system) paid off repeatedly — nearly every major fix was done from the outside, not on the broken system itself.
Today the system runs — LUKS, Btrfs, working snapshots and hibernation, no freezing. The road to that state was just considerably longer than "install and reboot."
One part of this migration — setting up LUKS disk unlock with a hardware FIDO2 key instead of a password — turned out to be enough of a saga on its own to deserve its own article: A Security Key Instead of a Password at Boot.
Dealing with something similar?
This is exactly the kind of work I take on for others too — Linux servers, Docker deployments, backups, and secure access setup.
View Linux & Docker services