Open Source  ·  MIT License
● v1.7.1  ·  Clone, netboot & fleet deploy

The AMI for your
Raspberry Pi

Nightly block-level backup to S3. Zero downtime. MariaDB is in read-only mode, not stopped. Zero config needed, it detects your DB automatically. Restore a complete bootable Pi to new hardware in one command.

Install on your Pi
curl -sL pi2s3.com/install | bash
SSH into your Pi and paste. Handles everything — dependencies, config, bucket creation, cron, dry-run, and first backup.
View on GitHub See what it does ↓
New in v1.7
📡 HTTP Netboot Coming Soon
Power + ethernet only. Pi 5 fetches the restore environment from boot.pi2s3.com — no SD card, no USB, no media.
💾 Recovery USB Coming Soon
Pre-built bootable image — flash, boot, wizard launches automatically. Coming in a future release.
🔁 Clone & stage
Restore to a second Pi, rename hostname, swap CF tunnel credentials and env vars before first boot — via --post-restore.
🚀 Fleet deploy Coming Soon
CSV manifest → SSH → restore in parallel. Classroom rollouts, office deployments, any number of Pis at once with --parallel.
🛡️ Hot standby New
Clone to a second Pi, assign its own CF tunnel, wire up a heartbeat Worker. When prod goes silent, DNS swaps to standby in under 2 minutes — automatically.
Zero-downtime by default. No config needed · One-command restore to new hardware · ~$3/month for 60-day history · No agents, no daemons, two shell scripts
pi@raspberrypi ~ backup
$ bash pi-image-backup.sh --force
# SET GLOBAL read_only=ON (containers stay up)
# partclone | pigz | aws s3 cp (no local file)
 
✔ DB lock acquired (site is live)
✔ Partition table saved
✔ nvme0n1p1 → S3 (2.1 GB, 3m 12s) probe: 4/4 pass
✔ nvme0n1p2 → S3 (1.4 GB, 2m 08s) probe: 2/2 pass
✔ DB unlocked (writes resumed)
✔ Manifest uploaded [2026-04-16]
 
$ # ntfy: "backup complete · probe 6/6 pass"
Where to go

Explore pi2s3

Pick a section — each page covers one part of the story in full detail.


Architecture

How it works

Two shell scripts: one runs nightly on the Pi, one restores everything to new hardware. No agents, no daemons, no cloud accounts beyond AWS.

BACKUP · STEP 01

Quiesce the database

MariaDB/MySQL: SET GLOBAL read_only=ON for the brief flush window — containers stay up, only writes are blocked. PostgreSQL: a CHECKPOINT, writes never blocked. Detected automatically whether your DB runs in Docker or natively. No DB: Docker is stopped briefly. Either way, crash-recovery makes the restore consistent.

BACKUP · STEP 02

Image with partclone

Reads only used blocks from each partition (not empty sectors). 954 GB NVMe at 28% full: partclone reads 267 GB, dd would read 954 GB.

BACKUP · STEP 03

Compress & stream

pigz compresses in parallel using all Pi 5 cores. Output streams directly to S3 with no local temp file and no second disk required.

BACKUP · STEP 04

Notify & verify

Docker restarts, manifest JSON uploaded, optional push notification via ntfy.sh. SHA256 verification confirms every file in S3.

RESTORE · STEP 01

Replay partition table

sfdisk restores the saved GPT layout to the new device. Partitions are recreated exactly: same sizes, same order.

RESTORE · STEP 02

Stream from S3

Each partition streams S3 → gunzip → partclone.restore. No local download. Works from any Linux machine attached to the target drive.

RESTORE · STEP 03

Boot & expand

Insert storage into the new Pi, power on. Raspberry Pi OS automatically expands the root filesystem on first boot. No extra config.

RESTORE · STEP 04

Verify

test-recovery.sh --post-boot checks OS, NVMe, Docker containers, Cloudflare tunnel, cron jobs, MariaDB, and HTTP. PASS/FAIL per check.

0 min
Downtime during backup
read-only holds writes for <10 s · containers stay up · site stays live
~35 min
Pi death to fully operational
Pi 5 · 954 GB NVMe · S3 af-south-1
3–5 GB
Compressed backup size
954 GB NVMe at 28% full · 267 GB of data → 4 GB · 67:1 ratio · partclone + pigz
~$3/mo
60-day retention
60 nightly images · S3 STANDARD_IA · af-south-1

Efficiency

partclone, not dd

dd reads every sector regardless of whether it contains data. partclone reads the filesystem allocation bitmap and skips unallocated blocks. Same result, a fraction of the work.

dd partclone
What it reads Every sector (used + empty) Used blocks only
Speed on 954 GB NVMe (28% full) ~90 min ~5 min
S3 upload size ~10 GB (compressed zeros) ~3–5 GB
Restore gunzip | dd partclone per partition
Docker downtime 60–90 min 5–15 min

Coverage

Everything. Block level.

Because it's a block level image of the full device, there's nothing to configure. Every file, database, container, service, and SSH key is included automatically. Works on both NVMe and SD card with auto detection of boot media.

Operating System

OS, kernel, packages, and systemd services including cloudflared, custom watchdogs, and any compiled binaries.

Docker Runtime

All images, volumes, networks, and compose configs. MariaDB data, WordPress uploads, application files: everything in /var/lib/docker.

Configuration & Secrets

.env files, config.env, credentials, authorized_keys, cron jobs, and logrotate rules. All restored exactly as is.

Boot Firmware

config.txt, cmdline.txt, and the full /boot/firmware partition. The restored Pi boots identically to the original.

Partition Table

GPT layout saved separately as a sfdisk dump and applied first on restore. Works across different NVMe sizes.

NVMe Tuning

Custom I/O scheduler settings, udev rules, and performance tuning survive the restore intact.


Cost

~$3/month for 60 days of history

At 3–5 GB per compressed image using S3 STANDARD_IA. Costs vary by region. af-south-1 (Cape Town) is slightly higher than us-east-1.

Retention S3 storage Monthly cost (STANDARD_IA)
7 images~25 GB<$1/month
30 images~120 GB~$2/month
60 images~240 GB~$3/month

S3 lifecycle policy is installed automatically by install.sh --setup. Images beyond MAX_IMAGES (default: 60) are deleted automatically. Switch to GLACIER_IR for long term cold storage at ~80% less cost.