Search terms: clone OS drive · migrate to SSD · smaller target · dd won’t fit · rsync clone · UEFI · ESP · grub-install · chroot · fstab UUID · unbootable after clone · media server SSD
Written 2026-08-10 for migrating media-server from a 931 GB WD Black HDD to a 500 GB Samsung 870 EVO, over a USB-SATA enclosure.
dd cannot be used here — the source device is larger than the target, so a block copy
refuses regardless of how little data is actually on it. This is a filesystem-level copy.
/dev/sda 931.5G GPT, UEFI
├─ sda1 512M vfat UUID=1FFA-656B → /boot/efi (6.2M used)
└─ sda2 931G ext4 UUID=569e1db7-940a-496e-a90c-0bdd60e5122d → / (88G used)
Swap is a file (/swapfile), not a partition — it copies with everything else, or can be
excluded and recreated. No separate /home. 88 GB of real data into a 500 GB target.
Read these before starting. Each has a guard step below.
/dev/sdb today may be
something else tomorrow. Always confirm by model and serial, never by device letter.Do this first, on the old drive, while everything still works. Without a before-picture, “did the clone work?” is a judgement call made by someone who wants the answer to be yes.
sudo /home/aztechguy/.local/bin/system-fingerprint.sh capture ~/fingerprint-BEFORE.txt
Use sudo — without it, root’s crontab and the Plex database are unreadable, and
the script will tell you so rather than quietly omitting them.
It records packages, enabled services (system and user), both crontabs, checksums of
key /etc files, whether every fstab target is actually mounted, file counts per
top-level directory, and Plex’s libraries with per-library file counts. Takes ~3
seconds, produces ~3,200 lines, changes nothing.
Copy it somewhere that isn’t the drive you’re about to replace:
scp ~/fingerprint-BEFORE.txt aztechguy@10.0.0.34:~/
Why this step exists. The 2026-08-11 media migration finished with 116 videos invisible to Plex — the folders were there, but had never been added as library roots. It went unnoticed for months because nothing had ever counted them. That was media. This is the operating system.
Plug the SSD into the USB-SATA enclosure, then:
lsblk -o NAME,SIZE,MODEL,SERIAL,TRAN,MOUNTPOINT
Model: Samsung SSD 870 EVO 500GB 465.8 GiB / 500 GB
Serial: S6PXNL0W611817D
Firmware: SVT02B6Q
SMART: PASSED · Power_On_Hours 0 · Power_Cycle_Count 3 · Total_LBAs_Written 0
Total_LBAs_Written: 0 is what proves a drive is genuinely new rather than a return or
refurb — a used drive always carries writes, however it was wiped. Worth checking on any new
disk before building on it:
sudo smartctl -a -d sat /dev/sdX | grep -iE 'Serial|Power_On_Hours|Power_Cycle|LBAs_Written|health'
ROTA=1over USB is expected and not a fault. Most USB-SATA bridges don’t pass the rotational flag through, so Linux assumes spinning rust. It reportsROTA=0only once the drive is on native SATA — which is why theROTAcheck belongs in Step 8 (after the swap), not here.
On a laptop this SSD appears as /dev/sda because the OS lives on NVMe. On media-server,
/dev/sda is already the OS drive you are replacing. Attached over USB the new SSD will land
somewhere like /dev/sde — and a command aimed at sda out of habit destroys the source.
Match the serial, never the letter:
lsblk -dno NAME,MODEL,SERIAL,SIZE,TRAN | grep S6PXNL0W611817D
That must print exactly one line, and its device name is the only one to use below.
Find the row with MODEL = your SSD and TRAN = usb. Write that device name down and
re-verify it before every destructive command:
TARGET=/dev/sdX # <-- set this once, from the output above
lsblk -dno MODEL,SERIAL,SIZE "$TARGET"
If that doesn’t print your new SSD’s model and serial, stop. Every command below writes to
$TARGET, and a wrong value destroys a drive holding your library.
sudo wipefs -a "$TARGET"
sudo parted -s "$TARGET" mklabel gpt
sudo parted -s "$TARGET" mkpart ESP fat32 1MiB 513MiB
sudo parted -s "$TARGET" set 1 esp on
sudo parted -s "$TARGET" mkpart root ext4 513MiB 100%
sudo parted -s "$TARGET" print
sudo mkfs.vfat -F32 "${TARGET}1"
sudo mkfs.ext4 -F "${TARGET}2"
Record the new UUIDs — Step 5 needs them:
lsblk -o NAME,UUID,FSTYPE "$TARGET"
Plex writes to its ~300 MB SQLite database constantly. Copying it live risks an inconsistent snapshot.
sudo systemctl stop plexmediaserver
Nothing else on this box writes heavily to the root filesystem. The trading crons and NAS mounts are unaffected.
sudo mkdir -p /mnt/newroot /mnt/newesp
sudo mount "${TARGET}2" /mnt/newroot
sudo mount "${TARGET}1" /mnt/newesp
Root filesystem. -x is the flag that matters most, and the one this runbook originally
omitted — see the warning below:
sudo rsync -aAXHvx --info=progress2 \
--exclude={"/dev/*","/proc/*","/sys/*","/run/*","/tmp/*","/mnt/*","/media/*","/lost+found","/swapfile"} \
/ /mnt/newroot/
⚠️
-x(–one-file-system) is not optionalLearned the hard way 2026-08-14. The first version of this runbook listed excludes for
/mnt/*and/media/*— which correctly stopped the NAS and the data drives — but nothing stopped/snap.Every snap revision is its own read-only squashfs mount. This machine has 68 of them. Without
-x, rsync descends into every one and expands compressed images into real files:du -sh --one-file-system /snap 152K <- the real directory structure du -sh /snap 41G <- crossing into all 68 mountsA 54 GB root copied as 97 GB. It fits on a 500 GB target, so nothing fails loudly — you just get 41 GB of shadow files hidden under mount points that snapd will later mount over, and a
snap refreshthat eventually fails trying tormdira non-empty revision directory.
-xmakes the/mnt/*and/media/*excludes redundant — it stops rsync at every filesystem boundary, including ones you didn’t think to enumerate. Enumerating mounts by hand is the mistake;-xcovers the ones you forgot and the ones added later.
-x--delete will not clean this up. rsync only deletes extraneous files in directories whose
contents it actually transferred — and with -x it never descends into a mount point, so it
never looks inside and never deletes there. Re-running with -x --delete fixes future copies
but leaves the existing junk untouched.
Empty the mount-point directories directly. This touches only paths derived from real
source mount points, and only two levels below /snap:
sudo bash -c '
findmnt -rno TARGET /mnt/newroot >/dev/null || { echo "ABORT: not mounted"; exit 1; }
echo "before: $(du -sh /mnt/newroot/snap | cut -f1), depth3+=$(find /mnt/newroot/snap -mindepth 3 | wc -l)"
findmnt -rno TARGET | grep "^/snap/" | while read m; do
t="/mnt/newroot$m"
case "$t" in /mnt/newroot/snap/*/*) [ -d "$t" ] && rm -rf "$t"/* "$t"/.[!.]* 2>/dev/null ;; esac
done
echo "after : $(du -sh /mnt/newroot/snap | cut -f1), depth1-2=$(find /mnt/newroot/snap -maxdepth 2 -mindepth 1 | wc -l)"
echo "source: $(du -sh --one-file-system /snap | cut -f1), depth1-2=$(find /snap -maxdepth 2 -mindepth 1 | wc -l)"'
The number that proves it worked is depth1-2, and it must match the source — 153 on this
machine. The mount-point directories themselves must survive, because snapd mounts each
squashfs over them at boot. You are emptying them, not removing them.
Then the ESP:
sudo rsync -aAXv /boot/efi/ /mnt/newesp/
Recreate the excluded mount points and swapfile:
sudo mkdir -p /mnt/newroot/{dev,proc,sys,run,tmp,mnt,media}
sudo chmod 1777 /mnt/newroot/tmp
sudo fallocate -l 2G /mnt/newroot/swapfile && sudo chmod 600 /mnt/newroot/swapfile && sudo mkswap /mnt/newroot/swapfile
(Match -l to your existing swapfile size — check with ls -lh /swapfile.)
The step most clones fail on. Get the new values:
NEW_ROOT=$(sudo blkid -s UUID -o value "${TARGET}2")
NEW_ESP=$(sudo blkid -s UUID -o value "${TARGET}1")
echo "root=$NEW_ROOT esp=$NEW_ESP"
Edit the copy, not the live one:
sudo nano /mnt/newroot/etc/fstab
Replace only the two OS lines:
UUID=<NEW_ROOT> / ext4 errors=remount-ro 0 1
UUID=<NEW_ESP> /boot/efi vfat umask=0077 0 1
Leave the data-drive lines exactly as they are — 4ce8b8b4…, 9c6a2a5e…, and the CIFS
mounts all live on other disks and their UUIDs do not change.
Verify no stale references survive:
grep -E "569e1db7|1FFA-656B" /mnt/newroot/etc/fstab && echo ">>> OLD UUIDs STILL PRESENT — fix before continuing" || echo "clean"
for d in dev dev/pts proc sys run; do sudo mount --bind /$d /mnt/newroot/$d; done
sudo mount --bind /mnt/newesp /mnt/newroot/boot/efi
sudo chroot /mnt/newroot /bin/bash -c '
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu --recheck &&
update-grub &&
echo "--- grub.cfg root reference ---" &&
grep -m2 "root=UUID" /boot/grub/grub.cfg
'
That last grep should show the new root UUID. If it shows the old one, update-grub read a
stale fstab — go back to Step 5.
Then unmount cleanly:
sudo umount -R /mnt/newroot/boot/efi /mnt/newroot/{run,sys,proc,dev/pts,dev} 2>/dev/null
sudo umount /mnt/newroot /mnt/newesp
sudo systemctl start plexmediaserver
Expect it to take a few attempts — and why
It did on 2026-08-14. Two things went wrong, both mundane, neither a clone failure:
1. Cables. Obvious in hindsight, easy to get wrong reaching into a case. Check SATA and power are properly seated on the new drive before closing anything up.
2. The firmware fell through to PXE / network boot. This is the one that looks catastrophic and isn’t. Removing the old drive invalidates the NVRAM boot entry that pointed at it. With no valid entry, the firmware works down its list and lands on network boot — so you get a PXE screen and assume the clone didn’t work.
The clone is fine. The boot order isn’t. Go into setup and explicitly select the Samsung SSD as the first boot device.
This is also why Step 6 uses
--no-nvramand why both EFI paths matter.--no-nvramavoids writing an entry pointing at a USB device path that would be wrong after the swap, and/EFI/BOOT/BOOTX64.EFIis the fallback the firmware tries once you point it at the right disk. Setting boot order by hand, once, is the intended final step — not a sign of trouble.The new drive inherits
/dev/sdaonce the old one is gone. Any note referring tosdaon this machine now means the SSD.
Keep the old drive intact until verification passes. It is your rollback — reconnect it and you’re back where you started.
lsblk -o NAME,SIZE,MODEL,MOUNTPOINT
findmnt / /boot/efi
df -h /
systemctl status plexmediaserver --no-pager | head -5
systemctl --failed --no-pager
findmnt -t cifs -o TARGET,OPTIONS | grep -oE "/mnt/nas[0-9.]*|vers=[0-9.]+"
Expected: root on the SSD, ~88 GB used of ~460 GB, Plex active, no failed units, all three CIFS
mounts back at vers=3.0.
Confirm the SSD is genuinely being used:
lsblk -dno NAME,ROTA,MODEL # ROTA=0 means SSD
sudo smartctl -A /dev/sda | grep -iE "Percentage Used|Data Units Written"
sudo /home/aztechguy/.local/bin/system-fingerprint.sh capture ~/fingerprint-AFTER.txt
/home/aztechguy/.local/bin/system-fingerprint.sh compare ~/fingerprint-BEFORE.txt ~/fingerprint-AFTER.txt
Read the output carefully — “no differences” is not automatically the pass condition here.
| Section | Expected after a good clone |
|---|---|
| PACKAGES | identical. Any difference means the copy was incomplete |
| SERVICES-ENABLED | identical, system and user both |
| CRON-USER / CRON-ROOT | identical |
| PLEX-LIBRARIES | identical root counts and file counts |
| MOUNTS-FSTAB | every target still MOUNTED |
cfg …/etc/fstab |
MUST DIFFER — see below |
FILECOUNT under /home, /etc |
small drift is normal between boots |
An
/etc/fstabchecksum that matches byte-for-byte is a FAILURE, not a pass. The clone needs new root and ESP UUIDs; an unchanged fstab means Step 5 never happened and the new disk is still mounting the old drive’s filesystems. It will appear to boot fine — right up until you disconnect the original.
Any !! UNAVAILABLE line means that section was never compared. Re-run the capture
with sudo rather than accepting an unverified section as a match.
Do not delete the old drive’s contents until the clone has run for a few days. It costs nothing to leave it on a shelf and it is the only complete rollback.
-aAXH preserves ACLs, extended attributes and hardlinks. Dropping -A or -X silently
loses file capabilities and SELinux/AppArmor labels — subtle and hard to diagnose later.
An alternative is restoring the Timeshift backup on sdd1 to the new disk from live
media. That handles partitioning and bootloader automatically and is less error-prone, at the
cost of restoring a snapshot rather than the current state.
Only snapshots from 2026-08-10 onward are complete. Every earlier one was taken while
Timeshift excluded /home — restoring one would produce a booting server with an empty home
directory: no SSH keys, no Discord webhook, no smart-health-check.sh. Check before trusting
a snapshot as a clone source:
S=/media/aztechguy/TimeshiftBackup/timeshift/snapshots/<snapshot>
du -sh "$S/localhost/home/aztechguy" # ~516M = complete; 4.0K = home was excluded
Timeshift generates /home/<user>/** and a /home/*/** catch-all at snapshot time regardless
of the config’s exclude array — deleting the home line does nothing. The fix is an include,
"+ /home/aztechguy/**", which is what the GUI’s Users tab writes. Its own cache rules
(/home/*/.cache and friends) sit earlier in the generated exclude.list and still win, which
is why a complete home reads 516M rather than the 735M on disk.
Related: System Update Runbook — run
post-update-smoke-test.sh after the swap to confirm nothing broke.