Skip to content

Instantly share code, notes, and snippets.

@assapir
Last active April 13, 2026 11:54
Show Gist options
  • Select an option

  • Save assapir/8081b2cedafdc7d0b0f79d79a7da1a04 to your computer and use it in GitHub Desktop.

Select an option

Save assapir/8081b2cedafdc7d0b0f79d79a7da1a04 to your computer and use it in GitHub Desktop.
Building an Arch Linux aarch64 VM image for UTM (Apple Silicon)

Building an Arch Linux aarch64 VM image for UTM (Apple Silicon)

Build a bootable qcow2 disk image of the unofficial Arch Linux aarch64 port from a Raspberry Pi or any aarch64 Linux system, for use with UTM on Apple Silicon Macs.

Prerequisites

On your aarch64 Linux host, install:

pacman -S gptfdisk qemu-img arch-install-scripts dosfstools

Build the image

# Create an 8GB raw disk
dd if=/dev/zero of=arch-aarch64.raw bs=1M count=8192

# Two partitions: FAT32 for /boot (ESP), ext4 for /
sgdisk -o \
  -n 1:0:+1G  -t 1:ef00 -c 1:boot \
  -n 2:0:0    -t 2:8300 -c 2:root \
  arch-aarch64.raw

sudo losetup -fP arch-aarch64.raw
LOOP=$(losetup -j arch-aarch64.raw | cut -d: -f1)

sudo mkfs.fat -F32 ${LOOP}p1
sudo mkfs.ext4 ${LOOP}p2

# FAT32 at /boot — this is important!
# The kernel and initramfs land directly on the EFI partition when installed.
sudo mount ${LOOP}p2 /mnt
sudo mkdir -p /mnt/boot
sudo mount ${LOOP}p1 /mnt/boot

Extract the bootstrap tarball

Check the tarballs directory for the latest version.

curl -LO https://arch-linux-repo.drzee.net/arch/tarballs/os/aarch64/archlinux-bootstrap-2026.03.15-aarch64.tar.zst
curl -LO https://arch-linux-repo.drzee.net/arch/tarballs/os/aarch64/archlinux-bootstrap-2026.03.15-aarch64.tar.zst.sig

# Verify signature
curl -sL https://arch-linux-repo.drzee.net/arch/extra/os/aarch64/public.key | gpg --import
gpg --verify archlinux-bootstrap-2026.03.15-aarch64.tar.zst.sig

# 'Good signature' is what matters here.
# If GPG also warns that the key is not certified or trusted,
# that's expected unless you've explicitly marked it as trusted
# in your personal keyring.

sudo tar xf archlinux-bootstrap-2026.03.15-aarch64.tar.zst -C /mnt --strip-components=1

Chroot and install

sudo arch-chroot /mnt

# Set up pacman keyring
pacman-key --init
pacman-key --populate archlinux

# Import the drzee signing key
curl -sL https://arch-linux-repo.drzee.net/arch/extra/os/aarch64/public.key -o /tmp/drzee.key
pacman-key --add /tmp/drzee.key
pacman-key --lsign-key 0CF25682E6BA0751

# Install base system + mainline kernel (not linux-rpi5 — this is a VM)
pacman -Syu
pacman -S base linux linux-firmware efibootmgr

# Set a root password — the bootstrap ships with root LOCKED
passwd

# mkinitcpio needs this file to exist
echo "KEYMAP=us" > /etc/vconsole.conf

# Build initramfs
mkinitcpio -P

# Install systemd-boot
bootctl install --esp-path=/boot

exit  # leave chroot

Configure the bootloader

Write the loader config from outside the chroot:

ROOT_UUID=$(sudo blkid -s UUID -o value ${LOOP}p2)

sudo tee /mnt/boot/loader/loader.conf << 'EOF'
default arch.conf
timeout 3
EOF

sudo mkdir -p /mnt/boot/loader/entries
sudo tee /mnt/boot/loader/entries/arch.conf << EOF
title   Arch Linux (aarch64)
linux   /vmlinuz-linux
initrd  /initramfs-linux.img
options root=UUID=${ROOT_UUID} rw console=ttyAMA0 console=tty0
EOF

Generate fstab and convert

sudo bash -c "sudo sh -c 'genfstab -U /mnt > /mnt/etc/fstab'"
sudo sudo umount -R /mnt
sudo losetup -d $LOOP

# Convert to qcow2 for UTM
qemu-img convert -f raw -O qcow2 -c arch-aarch64.raw arch-aarch64.qcow2
rm arch-aarch64.raw

Boot in UTM

In UTM: Virtualize → Other, skip the ISO, import the qcow2 as the drive, and set boot to UEFI.

Login: root with the password you set. sync does not show a progress bar. The easiest rule is: wait for the command to return and only unplug after sudo umount -R /mnt finishes. If you want to watch writeback activity, you can run watch -n1 "grep -E 'Dirty|Writeback' /proc/meminfo" in another terminal and wait for the numbers to get close to zero.

Alternative: direct kernel boot

UTM also supports booting without a bootloader. In the VM settings, point UTM directly at the kernel and initramfs files and pass boot arguments. You'll need to extract them from the image or download the linux package from drzee and unpack it.

Pitfalls I hit along the way

  • Don't use GRUB. Inside a chroot on a loop device, grub-mkconfig produced a config file full of null bytes. Even with a hand-written config, GRUB couldn't load the kernel from ext4 — it complained about "plain image kernel not supported." systemd-boot just works.
  • Mount FAT32 at /boot, not /boot/efi. With /boot/efi, the kernel and initramfs end up on the ext4 root partition, and the bootloader can't find them. With /boot, pacman installs them directly where systemd-boot expects them.
  • The bootstrap has root locked. There's a * in /etc/shadow. Not an empty password — no password works at all. You must run passwd before rebooting or you'll be locked out.
  • qemu-nbd and rm don't mix. If you modify a qcow2 via nbd, make sure to disconnect cleanly before deleting anything. I corrupted an image this way and had to rebuild from scratch.

Related

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment