You may build your using using
curl https://gist.githubusercontent.com/npenin/f3c9f59460f3fb6424be9aef5c6ce47b/raw/alpineos.sh | bash
#!/bin/sh | |
set -e | |
# ---------------------------------- | |
# Config | |
# ---------------------------------- | |
ALPINE_VERSION="latest-stable" | |
ARCH="aarch64" | |
MIRROR="https://dl-cdn.alpinelinux.org/alpine" | |
FIRMWARE_BASE_URL="https://github.com/raspberrypi/firmware/raw/master/boot" | |
WORKDIR="$(pwd)/alpine-pi-sdcard" | |
OUTPUT_DIR="$WORKDIR/boot" | |
ROOTFS_DIR="$WORKDIR/rootfs" | |
ROOTFS_TAR="$WORKDIR/rootfs.tar.gz" | |
ZIP_OUT="$(pwd)/alpineos.zip" | |
ALPINE_URL="$MIRROR/$ALPINE_VERSION/releases/$ARCH/alpine-minirootfs-latest-$ARCH.tar.gz" | |
mkdir -p "$OUTPUT_DIR" "$ROOTFS_DIR" | |
# ---------------------------------- | |
# Helper: cached curl | |
# ---------------------------------- | |
cached_curl() { | |
url="$1" | |
out="$2" | |
if [ -f "$out" ]; then | |
echo "Already cached: $out" | |
return 0 | |
fi | |
echo "Downloading: $url" | |
curl -L --fail --silent --show-error -o "$out" "$url" | |
} | |
# ---------------------------------- | |
# 1️⃣ Download Alpine minirootfs | |
# ---------------------------------- | |
echo "[1/7] Downloading Alpine minirootfs ($ARCH)..." | |
# Resolve latest stable full version | |
ALPINE_DIR=$(curl -s https://dl-cdn.alpinelinux.org/alpine/ | grep -Eo 'v[0-9]+\.[0-9]+/' | sort -V | tail -n1 | tr -d '/') | |
echo "[info] Latest stable directory: $ALPINE_DIR" | |
# Fetch latest version number inside that directory | |
FULL_VERSION=$(curl -s https://dl-cdn.alpinelinux.org/alpine/$ALPINE_DIR/releases/$ARCH/ | \ | |
grep -Eo 'alpine-minirootfs-[0-9]+\.[0-9]+\.[0-9]+-'$ARCH'.tar.gz' | head -n1 | \ | |
sed -E 's/alpine-minirootfs-(.*)-'$ARCH'.tar.gz/\1/') | |
echo "[info] Full version detected: $FULL_VERSION" | |
# Download URL | |
ALPINE_URL="https://dl-cdn.alpinelinux.org/alpine/$ALPINE_DIR/releases/$ARCH/alpine-minirootfs-$FULL_VERSION-$ARCH.tar.gz" | |
cached_curl "$ALPINE_URL" "$ROOTFS_TAR" | |
echo "[2/7] Alpine rootfs download complete." | |
# ---------------------------------- | |
# 2️⃣ Raspberry Pi firmware (optimized, POSIX) | |
# ---------------------------------- | |
echo "[3/7] Downloading Raspberry Pi firmware..." | |
download_once() { | |
url="$1" | |
out="$2" | |
if [ -f "$out" ]; then | |
echo "Already downloaded: $out" | |
return | |
fi | |
cached_curl "$url" "$out" | |
} | |
# Shared firmware for modern Pis | |
FILES="start4.elf start4cd.elf start4x.elf fixup4.dat fixup4cd.dat fixup4x.dat kernel7.img kernel7l.img kernel8.img" | |
for f in $FILES; do | |
URL="https://github.com/raspberrypi/firmware/raw/master/boot/$f" | |
download_once "$URL" "$OUTPUT_DIR/$f" | |
done | |
DTB_FILES=" | |
bcm2708-rpi-b-plus.dtb | |
bcm2708-rpi-b-rev1.dtb | |
bcm2708-rpi-b.dtb | |
bcm2708-rpi-cm.dtb | |
bcm2708-rpi-zero-w.dtb | |
bcm2708-rpi-zero.dtb | |
bcm2709-rpi-2-b.dtb | |
bcm2709-rpi-cm2.dtb | |
bcm2710-rpi-2-b.dtb | |
bcm2710-rpi-3-b-plus.dtb | |
bcm2710-rpi-3-b.dtb | |
bcm2710-rpi-cm0.dtb | |
bcm2710-rpi-cm3.dtb | |
bcm2710-rpi-zero-2-w.dtb | |
bcm2710-rpi-zero-2.dtb | |
bcm2711-rpi-4-b.dtb | |
bcm2711-rpi-400.dtb | |
bcm2711-rpi-cm4-io.dtb | |
bcm2711-rpi-cm4.dtb | |
bcm2711-rpi-cm4s.dtb | |
bcm2712-d-rpi-5-b.dtb | |
bcm2712-rpi-5-b.dtb | |
bcm2712-rpi-500.dtb | |
bcm2712-rpi-cm5-cm4io.dtb | |
bcm2712-rpi-cm5-cm5io.dtb | |
bcm2712-rpi-cm5l-cm4io.dtb | |
bcm2712-rpi-cm5l-cm5io.dtb | |
bcm2712d0-rpi-5-b.dtb | |
" | |
echo "[4/7] Downloading device trees..." | |
for dtb in $DTB_FILES; do | |
URL="https://github.com/raspberrypi/firmware/raw/master/boot/$dtb" | |
download_once "$URL" "$OUTPUT_DIR/$dtb" | |
done | |
# ---------------------------------- | |
# 3️⃣ Unpack rootfs | |
# ---------------------------------- | |
echo "[5/7] Extracting Alpine rootfs..." | |
tar -xzf "$ROOTFS_TAR" -C "$ROOTFS_DIR" | |
# ---------------------------------- | |
# 4️⃣ Add system management scripts | |
# ---------------------------------- | |
echo "[6/7] Adding update-system and rollback-system..." | |
mkdir -p "$ROOTFS_DIR/usr/local/sbin" | |
# --- update-system --- | |
cat > "$ROOTFS_DIR/usr/local/sbin/update-system" <<"EOF" | |
#!/bin/sh | |
set -e | |
MNT_ROOT="/mnt" | |
BTRFS_DEV="/dev/mmcblk0p2" # adjust as needed | |
MIRROR="https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/aarch64" | |
TMPDIR="/tmp/update" | |
NEW_SUBVOL="@system-new" | |
OLD_SUBVOL="@system-prev" | |
CURRENT_SUBVOL="@system" | |
echo "[update-system] Starting atomic update..." | |
mkdir -p "$MNT_ROOT" "$TMPDIR" | |
mount -o subvol=/ "$BTRFS_DEV" "$MNT_ROOT" | |
btrfs subvolume delete "$MNT_ROOT/$NEW_SUBVOL" 2>/dev/null || true | |
echo "[1/4] Downloading new rootfs..." | |
wget -qO "$TMPDIR/rootfs.tar.gz" "$MIRROR/alpine-minirootfs-$(uname -m).tar.gz" | |
echo "[2/4] Creating new snapshot..." | |
btrfs subvolume create "$MNT_ROOT/$NEW_SUBVOL" | |
tar -xzf "$TMPDIR/rootfs.tar.gz" -C "$MNT_ROOT/$NEW_SUBVOL" | |
echo "[3/4] Rotating subvolumes..." | |
if [ -d "$MNT_ROOT/$OLD_SUBVOL" ]; then | |
btrfs subvolume delete "$MNT_ROOT/$OLD_SUBVOL" | |
fi | |
mv "$MNT_ROOT/$CURRENT_SUBVOL" "$MNT_ROOT/$OLD_SUBVOL" | |
mv "$MNT_ROOT/$NEW_SUBVOL" "$MNT_ROOT/$CURRENT_SUBVOL" | |
umount "$MNT_ROOT" | |
rm -rf "$TMPDIR" | |
echo "[4/4] Update complete. Reboot to use new system." | |
echo "Use rollback-system to revert if needed." | |
EOF | |
chmod +x "$ROOTFS_DIR/usr/local/sbin/update-system" | |
# --- rollback-system --- | |
cat > "$ROOTFS_DIR/usr/local/sbin/rollback-system" <<"EOF" | |
#!/bin/sh | |
set -e | |
MNT_ROOT="/mnt" | |
BTRFS_DEV="/dev/mmcblk0p2" # adjust as needed | |
CURRENT_SUBVOL="@system" | |
PREV_SUBVOL="@system-prev" | |
BROKEN_SUBVOL="@system-broken" | |
echo "[rollback-system] Starting rollback..." | |
mkdir -p "$MNT_ROOT" | |
mount -o subvol=/ "$BTRFS_DEV" "$MNT_ROOT" | |
if [ ! -d "$MNT_ROOT/$PREV_SUBVOL" ]; then | |
echo "Error: No previous snapshot found." | |
umount "$MNT_ROOT" | |
exit 1 | |
fi | |
if [ -d "$MNT_ROOT/$BROKEN_SUBVOL" ]; then | |
btrfs subvolume delete "$MNT_ROOT/$BROKEN_SUBVOL" | |
fi | |
mv "$MNT_ROOT/$CURRENT_SUBVOL" "$MNT_ROOT/$BROKEN_SUBVOL" | |
mv "$MNT_ROOT/$PREV_SUBVOL" "$MNT_ROOT/$CURRENT_SUBVOL" | |
sync | |
umount "$MNT_ROOT" | |
echo "Rollback complete. Reboot to use previous system." | |
EOF | |
chmod +x "$ROOTFS_DIR/usr/local/sbin/rollback-system" | |
# ---------------------------------- | |
# 5️⃣ Add boot configuration | |
# ---------------------------------- | |
echo "[7/7] Creating boot configuration..." | |
cat > "$OUTPUT_DIR/cmdline.txt" <<'EOF' | |
console=tty1 root=/dev/mmcblk0p2 rootfstype=btrfs rootflags=subvol=@system rw rootwait | |
EOF | |
cat > "$OUTPUT_DIR/cmdline-prev.txt" <<'EOF' | |
console=tty1 root=/dev/mmcblk0p2 rootfstype=btrfs rootflags=subvol=@system-prev rw rootwait | |
EOF | |
cat > "$OUTPUT_DIR/config.txt" <<'EOF' | |
enable_uart=1 | |
arm_64bit=1 | |
kernel=kernel8.img | |
initramfs initramfs.img followkernel | |
EOF | |
# ---------------------------------- | |
# 6️⃣ Finalize image folder | |
# ---------------------------------- | |
echo "[✔] System ready in $WORKDIR" | |
echo "Creating distributable ZIP..." | |
cd "$WORKDIR" | |
zip -qr "$ZIP_OUT" . -x "rootfs.tar.gz" | |
cd - | |
echo "[✅] Image build complete: $ZIP_OUT" | |
echo "→ To use: format SD card as FAT32, unzip contents directly onto it." | |
echo "→ On first boot, system will self-install onto BTRFS and create subvolumes." | |