# Handoff: move `ballbox-first` Linux system to new 64GB microSD Date: 2026-05-04 Host: `ballbox-first` Target: migrate current Debian/Raspberry Pi OS system from current 16GB microSD to a new 64GB microSD ## Executive summary Best path: **offline block clone of current card to new 64GB card, then first boot from new card, expand rootfs, validate, keep old card untouched as rollback**. Reason: - Current system boots from microSD now. - `/mnt/rpi` exists on USB storage, but plan should not depend on it. - Free space on root is limited enough that creating local full-image files is a bad plan. - A live `dd` clone while booted from the source card is possible but less safe because root is changing during copy. - File-level rsync migration can work with almost no temp space, but it is more manual and easier to miss boot details. ## Current machine facts ### Current storage layout ```text Source device: /dev/mmcblk0 14.6G /dev/mmcblk0p1 512M vfat LABEL=bootfs UUID=F587-071F mounted at /boot/firmware /dev/mmcblk0p2 14.1G ext4 LABEL=rootfs UUID=d6944274-f2f7-4644-96a4-213c3b367f5c mounted at / Extra disk: /dev/sda1 238.7G ext4 mounted at /mnt/rpi ``` ### Current boot references `/proc/cmdline` ```text root=PARTUUID=9ecd9d57-02 ``` `/etc/fstab` ```text PARTUUID=9ecd9d57-01 /boot/firmware vfat defaults 0 2 PARTUUID=9ecd9d57-02 / ext4 defaults,noatime 0 1 UUID=9d779c82-e455-46a8-84d4-2df985173ef9 /mnt/rpi ext4 defaults,nofail 0 2 ``` Current source card partition table: ```text PTUUID 0x9ecd9d57 p1 start 16384 size 1048576 type c # 512 MiB boot p2 start 1064960 size 29658112 type 83 # root ``` ### Bootloader ```text BOOT_ORDER=0xf461 ``` This means the Pi 5 bootloader is flexible enough for SD/USB flows. Good for migration work. ### Current risk notes - System recently updated but **not yet rebooted** into latest kernel. - Running kernel: `6.12.47+rpt-rpi-2712` - Installed kernel: `6.12.75+rpt-rpi-2712` - Root free space last checked: about **1.6G free**. - Because root is tight, avoid any approach that writes a full disk image onto `/`. ## Goal Move system to new 64GB microSD with: - same hostname and services - same data and accounts - same bootability - larger root partition after migration - easy rollback by reinserting old card ## Strong recommendation ### Preferred method: offline card-to-card clone Use **another machine** or a **USB boot/rescue environment** to clone source card to target card while source card is not mounted as the active root filesystem. This is the safest method because: - exact copy of boot and root partitions - no dependency on `/mnt` - no need for large temp storage - rollback is trivial: swap back to old card ## Two good execution variants ### Variant A — easiest and safest if you have another machine Use a laptop/desktop with: - one microSD slot or USB reader for the **old/source** card - one USB reader for the **new/target** 64GB card Then: 1. power off Pi cleanly 2. remove old source card 3. connect old + new cards to the other machine 4. clone source -> target 5. reinsert new target in Pi 6. boot Pi 7. expand rootfs 8. validate 9. keep old card as rollback ### Variant B — if no second machine, use this Pi with rescue media Boot the Pi from a **temporary rescue OS** on USB or another spare microSD, then clone: - source old card inserted but not used as root - target 64GB card inserted via USB reader or SD slot This is also safe, but setup is more work. ## What not to do - Do **not** create a full image file on `/`. - Do **not** rely on `/mnt/rpi` as the only path. - Do **not** `dd` from the live source card to target while the Pi is actively using that source as `/` unless you accept filesystem inconsistency risk. - Do **not** destroy the old card until the new one has passed at least one reboot and service validation pass. ## Recommended runbook # Phase 0 — prepare before any cloning 1. Reboot current Pi once before migration. - Reason: finish the recent kernel update cleanly. - This reduces surprise during clone/boot validation. 2. Confirm system is healthy after reboot: ```bash uname -r systemctl is-system-running systemctl --failed --no-pager df -h / ``` 3. Record network access facts in case local display is unavailable: - hostname: `ballbox-first` - tailscale IP: `100.116.176.16` - hostname on tailnet: `ballbox-first.emperor-ratio.ts.net` 4. If remote-only recovery matters, make sure SSH is enabled and reachable before shutdown: ```bash systemctl is-enabled ssh systemctl status ssh --no-pager tailscale status --self ``` 5. Label cards physically: - old card: `OLD-16G-ROLLBACK` - new card: `NEW-64G-TARGET` # Phase 1 — clone source to target ## Option 1: clone on another Linux machine with `dd` Find devices carefully: ```bash lsblk -o NAME,SIZE,MODEL,TRAN ``` Assume: - source old card = `/dev/sdX` - target new card = `/dev/sdY` ### Safety checks ```bash sudo blkid /dev/sdX /dev/sdY /dev/sdX1 /dev/sdX2 /dev/sdY1 /dev/sdY2 ``` Make sure source and target are correct. If target auto-mounted, unmount it: ```bash sudo umount /dev/sdY1 /dev/sdY2 2>/dev/null || true sudo umount /dev/sdX1 /dev/sdX2 2>/dev/null || true ``` ### Clone command ```bash sudo dd if=/dev/sdX of=/dev/sdY bs=16M conv=fsync,status=progress sync ``` Notes: - `bs=16M` is a decent block size. - `conv=fsync` helps flush writes before exit. - This copies full source card, including bootloader-visible partition table and both partitions. ### Verify rough equality ```bash sudo fdisk -l /dev/sdX sudo fdisk -l /dev/sdY sudo blkid /dev/sdY1 /dev/sdY2 ``` At this point the target is a 64GB card containing a 16GB-sized layout. Root expansion comes later. ## Option 2: clone on another Linux machine with `rpi-imager` Possible if using Raspberry Pi Imager’s custom image write flow, but plain `dd` is simpler and more predictable for exact clone. ## Option 3: clone using rescue environment on Pi Once booted from rescue media, identify: - old source card device - new target card device Then run same `dd` flow: ```bash sudo dd if=/dev/mmcblkOLD of=/dev/sdTARGET bs=16M conv=fsync,status=progress sync ``` ## Optional stronger verification If you want high confidence before first boot: ```bash sudo cmp -n 100000000 /dev/sdX /dev/sdY ``` That only checks first 100MB. Full-device `cmp` is possible but slow. # Phase 2 — first boot from new 64GB card 1. Put **new 64GB card** into Pi SD slot. 2. Keep **old 16GB card** aside and untouched. 3. Boot Pi. 4. Watch for: - normal console boot - network comes up - SSH reachable - Tailscale reachable ## Immediate checks after login ```bash findmnt / findmnt /boot/firmware blkid /dev/mmcblk0 /dev/mmcblk0p1 /dev/mmcblk0p2 cat /proc/cmdline cat /etc/fstab df -h / uname -r systemctl is-system-running systemctl --failed --no-pager ``` Expected: - root still from `/dev/mmcblk0p2` - `root=PARTUUID=9ecd9d57-02` may still be identical to old clone until you randomize/repartition - system should behave exactly like old card ## Important note on duplicate PARTUUIDs A raw clone means old and new cards have the same disk identifier and partition PARTUUIDs. That is usually fine **as long as only one of those cards is connected at boot**. Do not boot with both clone and original attached if the kernel could see both and use identical PARTUUID-based references. Best practice after successful migration: - keep old card removed and stored - if you ever need both attached at once, first randomize the new disk ID and fix boot references For normal rollback storage, no issue. # Phase 3 — expand root partition to fill 64GB card After successful first boot from target card, expand partition 2 and then filesystem. ## Inspect target layout first ```bash sudo parted /dev/mmcblk0 print lsblk -o NAME,SIZE,FSTYPE,LABEL,UUID,PARTUUID,MOUNTPOINTS /dev/mmcblk0 ``` ## Expand partition 2 Using `parted`: ```bash sudo parted /dev/mmcblk0 --script 'print free' sudo parted /dev/mmcblk0 --script 'resizepart 2 100%' ``` Then ask kernel to reread: ```bash sudo partprobe /dev/mmcblk0 ``` If kernel cannot reread because root is live, reboot once, then continue. ## Expand ext4 filesystem ```bash sudo resize2fs /dev/mmcblk0p2 ``` ## Validate ```bash df -h / lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINTS /dev/mmcblk0 ``` Expected outcome: root grows close to full 64GB card capacity. # Phase 4 — optional cleanup after migration ## A. Regenerate unique disk ID / PARTUUIDs Only do this **after** the new card has booted successfully and you are ready to make it independent. Reason: - raw clone duplicates PTUUID/PARTUUIDs - harmless if old card is kept offline - cleaner long-term to make target unique ### Change DOS disk identifier Check current: ```bash sudo fdisk -l /dev/mmcblk0 | grep 'Disk identifier' ``` Set a new one using `fdisk` expert flow or `sfdisk --disk-id` if available. Example: ```bash sudo sfdisk --disk-id /dev/mmcblk0 0x1234abcd ``` Then new PARTUUIDs become: - `1234abcd-01` - `1234abcd-02` ### Update boot refs after changing disk ID Edit: - `/boot/firmware/cmdline.txt` - `/etc/fstab` Replace old PARTUUID `9ecd9d57-01/02` with new values. Then reboot and validate. ### Should you do this? - If old card will stay in a drawer: optional. - If old and new may both be attached to the same machine later: recommended. ## B. Grow boot partition? Not needed now. Current boot partition is 512MB and only ~79MB used. ## C. Remove old kernels after stable migration After a few successful boots, you can free some root space by pruning old kernels if package management has not already done it. Do this carefully and only after confirming new card is stable. # Fallback plan If new card fails to boot or behaves badly: 1. power off Pi 2. remove new card 3. reinsert old `OLD-16G-ROLLBACK` card 4. boot again Because clone method leaves old card untouched, rollback is near-instant. # Alternative plan if no offline clone path is available This is second-best but works without large temp storage and without `/mnt`. ## Live file-level migration to target card in USB reader ### Summary - partition new card manually - make filesystems - mount target under `/media/newroot` and `/media/newroot/boot/firmware` - rsync live system to it - copy boot files - fix target `fstab` and `cmdline.txt` - shut down cleanly - swap cards - boot target ### Why second-best - boot config must be correct manually - live source filesystem can change during rsync - usually you need at least two rsync passes and a final downtime window ### Sketch Assume target is `/dev/sda` in USB reader. Create partition table like source but larger root: ```bash sudo parted /dev/sda --script mklabel msdos sudo parted /dev/sda --script mkpart primary fat32 8MiB 520MiB sudo parted /dev/sda --script set 1 boot on sudo parted /dev/sda --script mkpart primary ext4 520MiB 100% sudo mkfs.vfat -F 32 -n bootfs /dev/sda1 sudo mkfs.ext4 -L rootfs /dev/sda2 ``` Mount target: ```bash sudo mkdir -p /media/newroot sudo mount /dev/sda2 /media/newroot sudo mkdir -p /media/newroot/boot/firmware sudo mount /dev/sda1 /media/newroot/boot/firmware ``` Copy root: ```bash sudo rsync -aHAXx --delete \ --exclude=/dev/* \ --exclude=/proc/* \ --exclude=/sys/* \ --exclude=/tmp/* \ --exclude=/run/* \ --exclude=/mnt/* \ --exclude=/media/* \ --exclude=/lost+found \ / /media/newroot/ ``` Copy boot files explicitly if needed: ```bash sudo rsync -aHAX /boot/firmware/ /media/newroot/boot/firmware/ ``` Get new target PARTUUIDs: ```bash sudo blkid /dev/sda1 /dev/sda2 /dev/sda ``` Then edit target files: - `/media/newroot/etc/fstab` - `/media/newroot/boot/firmware/cmdline.txt` Use target PARTUUIDs there. Before final cutover, stop mutable services if possible, run a second rsync pass, then shut down and swap cards. This method is viable, but only use it if offline cloning is not possible. # Recommended exact decision tree ## If you have access to a second machine with 2 readers Do this: - reboot current Pi once - shut down Pi - offline `dd` clone old -> new on second machine - boot new card - expand rootfs - validate ## If you have only one reader but another machine Still do offline clone if that machine can read one card at a time and has enough storage for a temporary image file **outside this Pi**. The temp image can live on the other machine. Fine. ## If you only have this Pi and one USB microSD reader Best safe path: - prepare temporary rescue boot media - boot rescue - clone old source -> new target directly - boot new target - expand rootfs ## If you cannot do rescue and must stay live Use the file-level rsync migration plan, not live `dd`. # Post-migration validation checklist Run all of these on the new card: ```bash uname -r systemctl is-system-running systemctl --failed --no-pager findmnt / findmnt /boot/firmware df -h / free -h tailscale status --self systemctl status nginx ssh tailscaled --no-pager journalctl -p err -b --no-pager | tail -n 100 ``` Also validate app/services important on this host: ```bash curl -fsS http://100.116.176.16:8082/api/status curl -fsS http://100.116.176.16:8091/api/status systemctl status copyparty qbittorrent-nox second-brain-bot --no-pager ``` Manual checks: - SSH works - Tailscale works - nginx routes still work - `/mnt/rpi` mounts correctly if USB disk is attached - desktop still boots if local UI matters # Risk register ## Risk: clone made from live filesystem Effect: silent inconsistency Mitigation: prefer offline clone or rescue boot ## Risk: wrong target device in `dd` Effect: destroys source or another disk Mitigation: triple-check with `lsblk`, `blkid`, physical unplug/replug, and sizes ## Risk: duplicate PARTUUIDs between old and new clone Effect: ambiguous boot if both visible together Mitigation: never connect both for normal boot, or randomize new disk ID after migration ## Risk: root partition not expanded Effect: system boots but still only uses old small size Mitigation: run `resizepart` + `resize2fs`, then validate with `df -h /` ## Risk: recent kernel update not active Effect: confusion during validation Mitigation: reboot old card before migration, then verify `uname -r` # Minimal command cheatsheet ## Before migration on current Pi ```bash sudo reboot uname -r systemctl --failed --no-pager ``` ## Offline clone ```bash sudo dd if=/dev/SOURCE of=/dev/TARGET bs=16M conv=fsync,status=progress sync ``` ## On first boot of new card: expand root ```bash sudo parted /dev/mmcblk0 --script 'resizepart 2 100%' sudo reboot sudo resize2fs /dev/mmcblk0p2 df -h / ``` ## Optional: make target disk ID unique ```bash sudo sfdisk --disk-id /dev/mmcblk0 0x1234abcd sudoedit /boot/firmware/cmdline.txt sudoedit /etc/fstab sudo reboot ``` # Recommended final handoff instruction If another agent/operator picks this up, tell them: - use **offline clone** as first choice - do **not** assume `/mnt/rpi` exists or has space - do **not** use live `dd` from active root unless explicitly accepting risk - keep old card untouched until new card passes full validation - after first good boot, expand root partition and optionally make PARTUUIDs unique # Local artifact This handoff file lives at: ```text /home/sebas/microsd-migration-handoff.md ```