raspberry-pi beginner 45 min

Raspberry Pi: back up and clone an SD card the safe way

Image a Raspberry Pi SD card with the right tool for each job: Raspberry Pi Imager for restores, dd and gzip for raw backups, rsync for daily copies.

Code available for: Python
Published Sep 22, 2026

Every Pi I run eventually gets the same moment: the SD card dies, or I break the OS tinkering, or I need a second Pi configured exactly like the first. The projects that survive this are the ones with a current image file sitting on a shelf. This tutorial covers the three backup tools I actually use, which one for which job, and the checks that keep a backup from being an unreadable file you discover during the emergency.

The trap I hit: I made a “backup” with a drag-and-drop copy of the files onto a USB stick. Files copied, nothing worked, because an SD card is not just files. It has partitions, a bootloader area, and permissions you cannot reproduce by copying. A real backup is a byte-level image (or a filesystem-level rsync with the right flags). The drag-and-drop version is a comfort object.

What you need

Needed

  • The Raspberry Pi whose card you want to back up
  • A second SD card or a USB card reader (e.g. any USB 3 reader, $8)
  • A destination with more free space than the card’s used space (a USB SSD or a spare drive; the image file is as big as the card)
  • A second computer (Windows, macOS, or Linux) for the imaging step, or a second Pi acting as the host

Nice to have

  • A second identical SD card for a “warm spare” you can swap in minutes
  • A USB SSD (faster than a spinning drive for image reads and writes)
  • A card reader with a write-protect switch for archiving golden images
  • A label or index card: the image date and what Pi it belongs to
  • Multimeter not needed here; this one is pure software

The three tools and when each one wins

JobToolWhy
Restore a fresh OS or reflashRaspberry Pi ImagerFast, official, writes with verification
Full byte-for-byte backupdd (plus gzip)Exact copy of everything, boot area included
Scheduled file-level backuprsyncFast, incremental, restartable

A full image of a 32 GB card takes 25-30 minutes at USB 3 speeds and eats 32 GB of disk. Rsync of the same card’s data might be 4 GB and two minutes. Most Pi owners should be running rsync weekly and making a full image only at milestones (right after setup works, right before a risky change, right after a risky change succeeds).

Setup: identify the card correctly

This is the safety-critical part. dd writes to a device, and if you aim it at the wrong one it will happily overwrite your own drive with zero confirmation. Always identify twice, write once.

On Linux (or a Pi acting as host), insert the reader and run:

lsblk

The card shows up as a disk with its size (e.g. sdb 32G with two partitions sdb1 and sdb2). Cross-check with:

sudo fdisk -l

If your destination drive also shows up here, note both names and keep them on screen while you work. The pattern if= means input file and of= means output file; getting those two backwards is the classic data-losing typo, and it happens to experienced people on bad days.

Method 1: full image with dd

Boot nothing, just the card in a reader on a Linux box (or another Pi):

sudo dd if=/dev/sdb of=pi-backup-$(date +%F).img bs=4M status=progress conv=fsync
sync

That is a raw image of the entire card, bootloader included, named with today’s date. bs=4M is speed, status=progress is sanity, and conv=fsync means the data is actually on disk when the command returns. The sync afterwards is for the write cache.

The image is the full card size even if the card is mostly empty. Shrink it with gzip:

sudo dd if=/dev/sdb bs=4M status=progress | gzip > pi-backup-$(date +%F).img.gz

A mostly-empty 32 GB card typically compresses to 2-6 GB. Restore is the reverse direction:

gunzip --stdout pi-backup-2026-09-23.img.gz | sudo dd of=/dev/sdb bs=4M status=progress conv=fsync
sync

Never write to the card while its partitions are mounted. If lsblk shows the partitions mounted (e.g. /media/pi/bootfs), unmount them first: sudo umount /dev/sdb1 /dev/sdb2. Imaging a mounted card gives you a corrupt image and a possibly corrupt card.

On Windows and macOS, the same job is one GUI app: Raspberry Pi Imager >> Choose OS >> scroll down to “Use custom” (pick your saved .img file), and for backups there is a “read from card” flow in the Imager’s OS chooser menu (Choose OS >> misc utility images; on Windows builds, use the reader’s drive letter with the OS-level disk tool diskpart/diskutil to identify it first). The GUI is slower than dd but shows a progress bar and verifies the write.

Method 2: rsync, the backup that runs weekly

The image is the milestone backup. Rsync is the rhythm. This is what keeps a running Pi safe week to week:

sudo rsync -aAXHv --delete --exclude={"/proc/*","/sys/*","/dev/*","/tmp/*","/run/*","/mnt/*","/media/*"} / /mnt/backup/pi-root/

The flags matter: -a preserves permissions and ownership, -A and -X preserve ACLs and extended attributes, -H preserves hard links, --delete makes the copy a mirror instead of an archive pile, and the excludes keep the Pi from copying its own live system directories (which would loop and fill the drive).

Run it from cron for an automatic weekly copy:

# /etc/cron.d/pi-backup
0 3 * * 1  root rsync -aAXH --delete --exclude={"/proc/*","/sys/*","/dev/*","/tmp/*","/run/*"} / /mnt/backup/pi-root/ >> /var/log/pi-backup.log 2>&1

Restoring from rsync is a copy in the other direction onto a freshly flashed card, then fixing the boot partition from a fresh Raspberry Pi OS image if the bootloader is what died. It is not byte-identical, but it preserves every file, permission, and config you care about, and it is small enough to run daily if you want.

Method 3: Raspberry Pi Imager for restores and spares

Raspberry Pi Imager >> Choose OS >> Raspberry Pi OS (other) >> pick the version, then Choose Storage >> the target card. Before writing, hit the gear icon (Imager >> OS customization) and preset the hostname, SSH, username, and Wi-Fi. A configured image means the replacement Pi boots straight into SSH with no monitor and no keyboard.

The “warm spare” habit is worth the cost of one extra card: image the working Pi’s card once, then write that image onto the spare card, and the spare lives in a drawer labeled with the date. Card dies Tuesday night, swap in the spare Wednesday morning, then rsync the recent files back on top. Downtime is one boot cycle.

The code

A small script that wraps the milestone image with the checks that make it trustworthy (size sanity, checksum, and a note in a backup log):

#!/usr/bin/env python3
"""sd_image.py: image an SD card with sanity checks and a checksum."""
import hashlib
import subprocess
import sys
from datetime import date

DEVICE = sys.argv[1] if len(sys.argv) > 1 else ""       # e.g. /dev/sdb
DEST = sys.argv[2] if len(sys.argv) > 2 else "/mnt/backup"
BLOCK = "4M"

def sh(cmd, timeout=None):
    return subprocess.run(cmd, shell=True, capture_output=True, text=True,
                          timeout=timeout)

def human(n):
    for unit in ("B", "K", "M", "G"):
        if n < 1024:
            return f"{n:.0f} {unit}"
        n /= 1024
    return f"{n:.1f} T"

def main():
    if not DEVICE:
        print("usage: sd_image.py /dev/sdX /mnt/backup")
        return 1
    # 1. device must exist and look like a disk
    r = sh(f"lsblk -b -n -o SIZE,TYPE {DEVICE}")
    if r.returncode != 0 or "disk" not in r.stdout:
        print(f"refusing: {DEVICE} is not a disk (lsblk says: {r.stdout!r})")
        return 1
    size = int(r.stdout.split()[0])
    print(f"target {DEVICE} size {human(size)}")
    if size < 1_000_000_000:
        print("refusing: suspiciously small device")
        return 1
    # 2. partitions must not be mounted
    r = sh(f"lsblk -n -o MOUNTPOINT {DEVICE}")
    mounts = [line.strip() for line in r.stdout.splitlines() if line.strip()]
    if mounts:
        print(f"refusing: partitions are mounted: {mounts}")
        print("unmount them first, e.g. sudo umount /dev/sdb1 /dev/sdb2")
        return 1
    # 3. image it
    stamp = date.today().isoformat()
    out = f"{DEST}/pi-backup-{stamp}.img"
    cmd = (f"sudo dd if={DEVICE} of={out} bs={BLOCK} status=progress "
           f"conv=fsync && sync")
    print("imaging...", flush=True)
    r = sh(cmd, timeout=7200)
    if r.returncode != 0:
        print(f"dd failed: {r.stderr[-500:]}")
        return 1
    # 4. checksum so a later restore is provable
    h = hashlib.sha256()
    with open(out, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    digest = h.hexdigest()
    with open(out + ".sha256", "w") as f:
        f.write(f"{digest}  {out}\n")
    print(f"done: {out}")
    print(f"sha256: {digest}")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

The refusals are the point. A backup script that can destroy a mounted drive or pick the wrong device is a loaded footgun; this one checks the device type, the size, and the mount state before it writes a byte, and it leaves a checksum so the image can be verified before a restore.

Verify the backup before you trust it

A backup you have never restored from is a hypothesis. The cheap verification, in order:

  1. The checksum matches (sha256sum -c pi-backup-2026-09-23.img.sha256).
  2. The image size matches the card size from lsblk.
  3. Mount the image read-only and look inside (Linux only, loop devices): sudo losetup -fP pi-backup-2026-09-23.img then list /mnt/loop2p2 (or wherever the root partition lands) and check /etc/hostname and /home.
  4. The gold standard: write it to a spare card and boot a Pi from it.

Items 1-3 take ten minutes and catch 90 percent of failures (truncated images, wrong device, corrupted writes). Item 4 is for the images you will bet an outage on.

What you learned

  • Drag-and-drop is not a backup of a Pi. You need an image or a filesystem-level copy.
  • dd makes exact images; gzip keeps them small; conv=fsync keeps them real.
  • rsync with -aAXH --delete and system-dir excludes is the weekly rhythm.
  • Verify (checksum, mount, boot) before you trust an image.

When something breaks

  • The image will not restore, or the restored card will not boot. Truncated image (disk filled during dd) is the usual cause. Check the file size against the card size, re-image with free space confirmed, and always conv=fsync plus sync.
  • dd finished instantly and the card is empty. You swapped if= and of=. The destination drive may now hold the SD’s old data (the card’s content is gone from the card but is likely ON the drive you mis-targeted). Stop writing to that drive and image it as the source to recover.
  • Rsync fills the destination drive. You forgot the excludes and it followed /proc and looped, or --delete was missing and every run added a full copy. Add the excludes and --delete, and prune old snapshots.
  • The restored Pi boots but the filesystem is read-only. The image was written to a card with a flaky connection or the card is worn out. Try a different card; SD cards have finite writes and old cards fail exactly like this.
  • Windows will not read the ext4 card or image. Normal. Windows cannot read ext4 natively; use WSL, or ext4-capable tools, or do the verify step on a Linux box.

What to build next

  • The Samba NAS tutorial: put /mnt/backup on a Pi NAS and your image and rsync jobs land on a second machine automatically.
  • The SQLite logging tutorial: application-level data survives even when the OS image is a milestone behind.
  • Pair this with the 4G LTE HAT tutorial for remote sites: rsync over the WireGuard bridge nightly, and the shed Pi’s config is always one image plus one sync away from restored.