Mass-provision Raspberry Pi units with Raspberry Pi OS, Tailscale, and sequential hostnames. Each provisioned Pi will:
- Boot from microSD with Raspberry Pi OS Lite (Trixie)
- Auto-configure via cloud-init (hostname, user account, SSH keys)
- Install Tailscale and join your Headscale tailnet
- Be accessible via Tailscale SSH — no password auth
Tested on Raspberry Pi 5 and Compute Module 5.
- Raspberry Pi units with PoE HATs or USB-C power
- microSD cards (≥16 GB), one per Pi
- PoE switch with sufficient power budget (~12W per Pi under load)
- Mac or Linux workstation for preparation
brew install coreutils
# No Docker required!- Headscale pre-auth key (reusable):
headscale preauthkeys create --user <YOUR_USER> --reusable --expiration 90d
- SSH public key (ed25519 recommended)
- Raspberry Pi OS Lite image (Trixie, 64-bit):
# Download from https://downloads.raspberrypi.com/raspios_lite_arm64/images/ # Use the .img.xz file directly — no decompression needed!
All settings are in a single config.env file:
cp config.env.example config.env
# Edit config.env with your values
⚠️ Never commitconfig.envto git — it contains your Tailscale auth key.
See config.env.example for all available settings.
A USB stick that auto-provisions Pis when plugged in. See usb-provisioner/README.md.
Workflow: Insert microSD → Plug USB → Connect PoE → Wait for LED blink → Done.
Flash microSD cards one at a time from your Mac. See batch-flash/README.md.
Workflow: Insert SD card → Script flashes & configures → Eject → Label → Repeat.
After provisioning, verify your fleet:
./verify/verify-fleet.shThis checks each Pi's connectivity via tailscale ping. See the script output for troubleshooting guidance on offline units.
The cloud-init/ directory contains the templates injected onto each microSD card:
| File | Purpose |
|---|---|
user-data.template |
User account, SSH keys, Tailscale setup, packages |
meta-data.template |
Instance ID and hostname |
network-config.template |
Ethernet DHCP configuration |
Placeholders (e.g., __HOSTNAME__, __TAILSCALE_AUTH_KEY__) are substituted at provisioning time from your config.env.
- Auth key redaction: The Tailscale auth key is automatically removed from cloud-init artifacts on first boot
- SSH key-only: Password auth is disabled over SSH; only your configured SSH key grants access
- Console password: Optional — set
ADMIN_PASSWORDin config.env for local keyboard+monitor login - Tailscale SSH: Standard SSH is replaced by Tailscale SSH (
--accept-risk=lose-ssh); manage access through Headscale ACLs - Post-provisioning: Revoke the reusable pre-auth key:
headscale preauthkeys expire --id <KEY_ID>
Default EEPROM boot order is 0xf41 (SD → USB → loop). For the USB provisioner:
- Blank microSD: Pi can't boot from SD, falls through to USB ✅
- Previously-used microSD: Pi boots from SD instead of USB ❌ — wipe or use a new card
If you need to change boot order: sudo rpi-eeprom-config --edit on a running Pi.
| State | Power Draw |
|---|---|
| Idle | ~5W |
| Flashing SD (provisioning) | ~8–12W |
| PoE HAT max | 25.5W |
A 48-port PoE switch with 740W budget can handle ~30–40 Pis simultaneously.
For parallel provisioning, create multiple USB sticks with different starting counters by changing HOSTNAME_START in config.env before each prepare-usb.sh run.