Installing Arch Linux ARM with QEMU and Moving It into UTM
Installing Arch Linux ARM with QEMU and Moving It into UTM
I wanted a small Linux VM on Apple Silicon: install Arch Linux ARM through QEMU, run Docker, then manage it in UTM. Separate installation and import KBs describe one workflow.
The record establishes disk boot and successful UTM login. Its claims about HVF, distribution source, sandboxing, and nested virtualization need qualification before reuse.
Distinguish architecture, accelerator, and backend
An ARM64 guest matches Apple Silicon, but a QEMU command does not automatically establish HVF use. The original used -cpu cortex-a72 without -accel hvf. It documents a bootable configuration, not verified hardware acceleration or performance.
uname -m
sysctl kern.hv_support
qemu-system-aarch64 -accel help
qemu-system-aarch64 --versionHost capability and available accelerators are not the active VM configuration. Archive actual arguments, QEMU output, and the selected UTM backend.
| Route | Interpretation |
|---|---|
| QEMU TCG | Software emulation; performance requires measurement |
| QEMU HVF | Hypervisor.framework acceleration with supported architecture and configuration |
| UTM QEMU backend | QEMU devices and parameters with supported acceleration or emulation |
| UTM Apple backend | Virtualization.framework; QEMU settings and qcow2 compatibility are not automatically transferable |
Delete the blanket claim that all Apple Silicon lacks nesting. Apple exposes a Virtualization.framework support query; M3-and-later hardware, OS, backend, and configuration matter. Booting this guest did not validate /dev/kvm or a second-level VM. Apple nesting capability
Record installation inputs
Prepare an aarch64 Live ISO, Arch Linux ARM rootfs, fresh qcow2 disk, and AArch64 UEFI firmware. The record selected Alpine standard after another image produced an X64 EFI error. Inspect EFI/BOOT/BOOTAA64.EFI in the actual image instead of declaring every Alpine virt release unusable.
Archive downloads with version, hash, and source; latest archives and Homebrew packages change. These commands assume downloaded files rather than prescribing historical release numbers:
brew install qemu
mkdir -p "$HOME/VMs/arch-arm"
qemu-img create -f qcow2 "$HOME/VMs/arch-arm/disk.qcow2" 30G
qemu-img info "$HOME/VMs/arch-arm/disk.qcow2"
shasum -a 256 "$HOME/VMs/arch-arm/live-aarch64.iso" "$HOME/VMs/arch-arm/rootfs.tar.gz"30 GiB is virtual capacity; host allocation grows with writes. Sparse creation does not eliminate upgrade, copying, or backup capacity requirements. Check the Arch Linux ARM Generic AArch64 guide and select media from Alpine downloads.
Boot the Live environment with an explicit route
This revised TCG example retains the original cortex-a72 route. Test -accel hvf -cpu host separately when acceleration is required. Resolve and inspect the firmware path instead of assuming every Homebrew installation uses the same location.
arch_vm_dir="$HOME/VMs/arch-arm"
arch_qemu_prefix="$(brew --prefix qemu)"
qemu-system-aarch64 \
-machine virt -accel tcg -cpu cortex-a72 -smp 2 -m 2048 \
-bios "$arch_qemu_prefix/share/qemu/edk2-aarch64-code.fd" \
-drive if=none,format=qcow2,id=hd0,file="$arch_vm_dir/disk.qcow2" \
-device virtio-blk-pci,drive=hd0 \
-cdrom "$arch_vm_dir/live-aarch64.iso" -boot d \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 \
-device virtio-net-pci,netdev=net0 -nographicvirt is a generic virtual platform, not a physical Raspberry Pi. SSH forwarding binds only to the host loopback address. A forwarded port cannot log in until the guest SSH service is ready; use serial for installation first. QEMU virt
Partition only the verified new guest disk
The following Live-guest commands overwrite /dev/vda. Inspect capacity, existing partitions, and mounts first; never substitute a host disk casually.
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS
apk add util-linux e2fsprogs dosfstools libarchive-tools
sfdisk /dev/vda <<'EOF'
label: gpt
,512M,U
,+,L
EOF
mkfs.vfat -F 32 /dev/vda1
mkfs.ext4 /dev/vda2
mount /dev/vda2 /mnt
mkdir -p /mnt/boot
mount /dev/vda1 /mnt/bootUse a 512 MiB FAT32 ESP and ext4 root. Make the verified rootfs accessible inside the Live environment, inspect its top-level structure, then extract as root with permissions and links preserved:
tar -tzf /tmp/rootfs.tar.gz | head -20
bsdtar -xpf /tmp/rootfs.tar.gz -C /mnt
blkid /dev/vda1 /dev/vda2Write actual UUIDs into /mnt/etc/fstab. Configure target networking separately from the Live environment: download success does not prove networking survives the reboot into Arch. Set hostname, time zone, locale, and generate the selected locale in the target system.
Keep the running kernel and modules aligned
The original loader referenced /vmlinuz-linux, while a later package updated /boot/Image. A stale copied boot file explains one class of “package updated, running kernel unchanged” failures. One initramfs missing drivers does not prove ARM64 direct-kernel boot is impossible.
Inspect the actual kernel, EFI compatibility, and VirtIO/filesystem support in initramfs. If retaining a copied vmlinuz-linux, document how upgrades refresh it and verify after every reboot:
uname -r
ls -l /boot/Image /boot/vmlinuz-linux /boot/initramfs-linux.img
ls /usr/lib/modules
cat /boot/loader/entries/arch.confFor systemd-boot, prepare chroot /proc, /sys, /dev, verify the ESP mount, and run bootctl install. If firmware-variable writes fail, also inspect the architecture-correct fallback path. One copied-file message is insufficient acceptance. bootctl
For Docker netfilter errors, compare uname -r, module directories, and loader entries. The historical kernel repair restored Docker, but identical text elsewhere can have other causes. Preserve service logs before changing network or host firewall settings.
This revised EFI-stub Image example avoids a stale copied kernel. It assumes the verified partition layout, mounted ESP, and actual initramfs path. Fix missing files or required boot drivers before applying it. The official rootfs has preset accounts; change credentials for retained accounts before enabling network exposure.
# Run inside the Live guest after extracting the target rootfs.
arch_root_uuid="$(blkid -s UUID -o value /dev/vda2)"
arch_esp_uuid="$(blkid -s UUID -o value /dev/vda1)"
printf 'UUID=%s / ext4 defaults 0 1\nUUID=%s /boot vfat defaults 0 2\n' \
"$arch_root_uuid" "$arch_esp_uuid" > /mnt/etc/fstab
mount -t proc proc /mnt/proc
mount --rbind /sys /mnt/sys
mount --make-rslave /mnt/sys
mount --rbind /dev /mnt/dev
mount --make-rslave /mnt/dev
chroot /mnt /bin/bash# Run in the target chroot; inspect paths before writing the loader entry.
ls -l /boot/Image /boot/initramfs-linux.img
bootctl --esp-path=/boot install
mkdir -p /boot/loader/entries
printf 'default arch.conf\ntimeout 3\n' > /boot/loader/loader.conf
arch_root_uuid="$(blkid -s UUID -o value /dev/vda2)"
cat > /boot/loader/entries/arch.conf <<EOF
title Arch Linux ARM
linux /Image
initrd /initramfs-linux.img
options root=UUID=$arch_root_uuid rw console=ttyAMA0
EOF
pacman-key --init
pacman-key --populate archlinuxarm
passwd
exitAfter leaving chroot, run sync, unmount the target tree from the Live environment, and shut down normally. Remove -cdrom and -boot d for disk boot while retaining the same devices. Verify mounts, kernel, and networking before a full pacman -Syu and reboot. Install Docker with pacman -S docker and systemctl enable --now docker, accepting logs and a real container run. Historical screenshots do not validate execution of this revised example.
Import hardware configuration with the disk
Shut down the guest, confirm QEMU exited, and back up the disk. In UTM select the QEMU backend, aarch64 architecture, matching disk interface, and UEFI. A qcow2 import does not include all command-line devices, forwarding rules, or firmware variables. UTM drive import
The record first showed No such file or directory: the package contained EFI variables but no expected disk. A symlink to the external volume changed the error to Operation not permitted. Copying the disk into the configured package location while stopped led to successful boot.
| Error | Inspect first | Supported conclusion |
|---|---|---|
| No such file or directory | Referenced path, package contents, mounted volume | Expected file was not opened; not proof of a UTM bug |
| Operation not permitted | Resolved path, authorization, process rights, entitlements | Access was denied; the exact restriction needs inspection |
| Boot succeeds with divergent data | Active image and location of earlier changes | A copied disk may now be in use |
A container path does not prove App Store origin, and Homebrew installation does not prove absence of sandboxing. Symlinks grant no authority to access their targets. Prefer the supported import interface and file authorization, not blind permission changes or hard-link workarounds.
Accept the migration and select one working copy
Verify login, OS identity, mounts, network, Docker, and persistent state after reboot. The historical CLI used ttyAMA0, while UTM displayed tty1; a different terminal does not imply a different OS.
cat /etc/os-release
uname -r
findmnt /
findmnt /boot
ip address
systemctl status docker --no-pagerChoose one disk as the daily authoritative image. Copies diverge; do not run both and casually synchronize them. Stop all writers, back up the destination, and perform a directional replacement or migration. Back up firmware variables, UTM settings, disk, and installation provenance together. Historical installation/import succeeded; revised commands, HVF, and nesting still require version-specific validation.
The date is the main KB's first Git commit date, 2026-07-15 (UTC+8), commit 039bdbb. Merged sources are retained in metadata; historical operation dates are separate from repository dates. Revised configuration examples were not executed on production devices.
