====== SD card backup and restore on Linux with dd, zstd and PiShrink ======
Cloning a card to a file, and writing that file back to the same or a
different card. Covers the compression and shrinking steps that make an image
worth keeping, and the checks that stop you writing over the wrong disk.
Verified on ''coreutils 9.5'', ''zstd 1.5.6'', ''pishrink v0.1.4'' and
''f3 8.0'' under CachyOS.
**The card is ''/dev/sdb'' — the whole device, with no partition number.
Everything here writes to the raw device, so getting this wrong destroys
another disk.**
===== Quick Reference =====
^ Task ^ Command ^
| Identify the card | ''lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS,MODEL,TRAN'' |
| Unmount auto-mounted partitions | ''udisksctl unmount -b /dev/sdb1'' |
| Back up, compressed | ''dd if=/dev/sdb bs=4M status=progress \| zstd -T0 -6 -o card.img.zst'' |
| Shrink a Pi image | ''sudo pishrink.sh -Z card.img'' |
| Restore, compressed | ''zstd -dc card.img.zst \| sudo dd of=/dev/sdb bs=4M status=progress conv=fsync'' |
| Verify the write | ''sudo cmp -n $(stat -c%s card.img) card.img /dev/sdb'' |
| Test a suspect card | ''sudo f3probe --destructive --time-ops /dev/sdb'' |
===== Identify the card =====
Do this every time. Device letters are assigned in the order things are
plugged in, so the card that was ''/dev/sdb'' yesterday can be ''/dev/sdc''
today.
**# List block devices with size, model and transport:**
lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS,MODEL,TRAN
The card is the one whose size matches and whose ''TRAN'' is ''usb'' for a
reader, or ''mmc'' for a built-in slot. Its partitions appear indented
beneath it as ''sdb1'', ''sdb2''.
**# Confirm by watching the kernel as you plug it in:**
sudo dmesg -w
Unambiguous. Plug the reader in with this running and the device name
appears in the last few lines.
**# Unmount anything the desktop auto-mounted:**
udisksctl unmount -b /dev/sdb1
KDE mounts removable media on insertion. ''dd'' will happily read a mounted
device, but writing to one produces a corrupt result — and the partitions
must be unmounted, not the whole device.
===== Backing up =====
==== Straight image ====
**# Copy the whole card to a file:**
sudo dd if=/dev/sdb of=card.img bs=4M status=progress conv=fsync
The image is the **full capacity of the card**, not the used space. A 32 GB
card produces a 32 GB file whether it holds 2 GB or 30 GB.
==== Compressed image ====
Almost always what you want. Empty space compresses to nearly nothing.
**# Image and compress in one pass:**
sudo dd if=/dev/sdb bs=4M status=progress | zstd -T0 -6 -o card.img.zst
''-T0'' uses every core. ''-6'' is the sweet spot — ''-19'' takes many times
longer for a few percent.
**# Zero the free space first, so it actually compresses:**
sudo dd if=/dev/zero of=/run/media/josephk/rootfs/zero.fill bs=1M status=progress; sudo rm /run/media/josephk/rootfs/zero.fill
Deleted files leave their contents on the card. Without this, a card that
has had 20 GB written and deleted still compresses like 20 GB of data.
Mount the card, run this, unmount, then image.
==== Shrunk image, for Raspberry Pi cards ====
**# Shrink the image and compress it:**
sudo pishrink.sh -Z card.img
PiShrink resizes the last partition down to its used size and adds a
first-boot script that expands it again on the target card. ''-Z'' compresses
with xz, ''-z'' with gzip. The result restores to any card at least as large
as the shrunk image, which is the whole point — see gotcha 2.
Only works on images with a Linux filesystem as the last partition. Fine for
Raspberry Pi OS. Useless for a FAT-only card.
===== Restoring =====
**# Write a plain image back:**
sudo dd if=card.img of=/dev/sdb bs=4M status=progress conv=fsync
**# Write a zstd image back without unpacking it first:**
zstd -dc card.img.zst | sudo dd of=/dev/sdb bs=4M status=progress conv=fsync
**# Clear stale signatures before writing a smaller image:**
sudo wipefs -a /dev/sdb
Skip this and the kernel may find an old partition table beyond the end of
the new image and present phantom partitions.
**# Flush and confirm the write finished:**
sync
''conv=fsync'' already does this, but ''sync'' costs nothing and the card's
own controller can still be writing after ''dd'' returns. Wait for the
reader's LED before pulling it.
==== The gotchas that will waste your afternoon ====
**# 1. Cards of the same nominal size are not the same size.**
Two 32 GB cards from different makers differ by tens of megabytes. Writing a
31.9 GB image to a 31.8 GB card runs to the end and dies:
dd: error writing '/dev/sdb': No space left on device
The partition table is written, the last partition is truncated, and the
card looks fine until the filesystem faults. Shrink the image with PiShrink,
or keep the smallest card you own as the master.
**# 2. ''dd'' reports success on a card that is silently dropping writes.**
Counterfeit and worn cards acknowledge writes they never commit. Nothing in
the ''dd'' output betrays it. Test the card, do not trust it.
sudo f3probe --destructive --time-ops /dev/sdb
**# 3. The card reader is as likely to be at fault as the card.**
A flaky USB reader corrupts the transfer, then verification passes because
it reads back through the same broken path. If an image fails twice, change
the reader before you change anything else.
**# 4. A partition number in ''of='' silently makes a useless image.**
''of=/dev/sdb1'' backs up one filesystem with no partition table and no boot
sector. It restores to nothing usable. Always the bare device.
===== Verifying =====
**# Compare the card against the image, image length only:**
sudo cmp -n $(stat -c%s card.img) card.img /dev/sdb
Silence means identical. Without ''-n'', ''cmp'' reports a spurious
difference at EOF because the card is larger than the image.
**# Verify a compressed image without writing it anywhere:**
zstd -t card.img.zst
Checks the archive's integrity. Do this before you rely on a backup, not
after the original card has died.
**# Checksum the image for later comparison:**
sha256sum card.img > card.img.sha256
===== Speed =====
A USB 3 reader with a decent card runs at 40–90 MB/s, so a full 32 GB card
takes **6 to 13 minutes** each way. A USB 2 reader runs at 10–15 MB/s —
40 minutes or more. Compression is not the bottleneck at ''-6''; the card
is.
**# Watch throughput with a progress bar instead:**
sudo dd if=/dev/sdb bs=4M | pv -s 32G | zstd -T0 -6 -o card.img.zst
''status=progress'' is enough for most purposes. Use ''pv'' when you want an
ETA.
===== Troubleshooting =====
^ Symptom ^ Cause ^ Fix ^
| ''No space left on device'' near the end | target card smaller than source | shrink with ''pishrink.sh'', or use a larger card |
| ''Device or resource busy'' | partition auto-mounted | ''udisksctl unmount -b /dev/sdb1'' |
| Restored card will not boot | wrote to ''/dev/sdb1'' not ''/dev/sdb'' | re-image the whole device |
| Image is full card size | expected — that is raw ''dd'' | pipe through ''zstd'', zero free space first |
| Compressed image barely smaller | free space full of deleted data | zero-fill before imaging |
| ''dd'' succeeds, card fails later | card dropping writes | ''f3probe'', then replace the card |
| Phantom partitions after restore | stale signatures past the image end | ''wipefs -a /dev/sdb'' before writing |
| Verification fails twice on different cards | bad reader | change the reader |
===== Which one should I use? =====
^ ^ Raw ''dd'' image ^ PiShrink + zstd ^
| Any filesystem, any card | **yes** | Linux last partition only |
| Storage cost of the image | full card size | used space only |
| Restores to a slightly smaller card | no | **yes** |
| Time to create | fastest | slower, one extra pass |
| Bit-identical to the original | **yes** | no, partition is resized |
Rule of thumb: **PiShrink for anything you will keep or hand to someone else,
raw ''dd'' when you need an exact forensic copy or the card is not Linux**.