Skip to content

Instantly share code, notes, and snippets.

@npenin
Last active October 10, 2025 05:19
Show Gist options
  • Save npenin/f3c9f59460f3fb6424be9aef5c6ce47b to your computer and use it in GitHub Desktop.
Save npenin/f3c9f59460f3fb6424be9aef5c6ce47b to your computer and use it in GitHub Desktop.
Create an alpine os on BTRFS with system snapshots

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."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment