Creating portable, automated NixOS VMs on Apple Silicon Macs using UTM virtualization.
URL: https://github.com/ciderale/nixos-utm Purpose: Automate creation of UTM-based NixOS VMs Key Features:
- Wraps
nixos-anywhereto eliminate manual provisioning steps - Apple Virtualization backend support w/ Rosetta2 for x86_64 emulation
- Automated setup:
nix run github:ciderale/nixos-utm#nixosCreate .#utm - IP retrieval via ARP cache lookup (MAC-based)
- Deploy config updates without VM recreation
Limitations: utmctl lacks full Apple backend support for some operations
URL: https://github.com/onnimonni/nixos-utm-vm-example Purpose: Practical NixOS VM example for UTM on Apple Silicon Key Features:
- Flake-based config management
- SSH key auth (ed25519)
- Avahi integration for
.localhostname access - Remote builder pattern for x86_64 Linux builds from ARM host
- Rosetta emulation support
URL: https://github.com/utmapp/UTM Description: Full-featured VM host for iOS/macOS (QEMU-based) Languages: Swift (primary), Objective-C, Shell, Python, C
tell application "UTM"
set vmList to every virtual machine
set myVM to first virtual machine whose name is "Ubuntu Server"
start myVM
suspend myVM
stop myVM
end tellutm://run?name=MyVM
utm://pause?name=MyVM
utm://stop?name=MyVM
- Programmatic VM creation/config via
UTMQemuConfiguration - Drive, network, port forwarding, display settings
- VLAN configuration, multiple network interfaces
device = "/dev/disk/by-label/nixos" # Works across vda/sdaBenefit: Same config works for ARM64 (virtualized) & x86_64 (emulated) VMs
- Lock dependency versions
- Modular organization: CLI tools, apps, shell config, dev envs
- Architecture-specific outputs
- Declarative user environment
nix run . switchactivates configs
- Architecture: ARM64 (aarch64)
- Memory: 8-16GB
- CPU: 4+ cores
- Display: virtio-ramfb-gl (GPU acceleration)
- Network: Shared mode w/ port forwarding (SSH: 22 → 2222)
- Boot: UEFI, RNG Device, Hypervisor enabled
Enable x86_64 emulation:
virtualisation.rosetta.enable = true;# Avahi for .local hostname access
services.avahi.enable = true;# Download NixOS ISO (ARM64 minimal with latest kernel)
# From: https://nixos.org/download.html#nixos-iso
# Or Hydra: https://hydra.nixos.org/job/nixos/trunk-combined/nixos.iso_minimal.aarch64-linux
# Verify ISO hash
shasum -a 256 nixos-minimal-*.iso
# Create directory for your VM config (optional, for later)
mkdir -p ~/nix-configs/utm-vm
cd ~/nix-configs/utm-vm- Open UTM → "Create a New Virtual Machine"
- Select "Virtualize" (not Emulate)
- Operating System: Linux
- Boot ISO: Select downloaded NixOS ISO
- Hardware:
- Memory: 8192 MB (8GB) minimum
- CPU Cores: 4
- Storage: 100GB (or as needed)
- Shared Directory: Skip for now (configure post-install)
- Summary:
- Name:
nixos-dev - Check "Open VM Settings"
- Name:
- Settings Tweaks:
- System → Enable "UEFI Boot"
- Display → "virtio-ramfb-gl" for GPU acceleration
- Network → "Shared Network" + check "Enable Port Forwarding"
- Guest Port: 22, Host: 2222, Protocol: TCP (for SSH)
- Save & Start VM
# Boot into NixOS installer, switch to root
sudo su
# Identify disk (usually /dev/vda for UTM)
lsblk
# Partition with parted
parted /dev/vda -- mklabel gpt
parted /dev/vda -- mkpart primary 512MB 100%
parted /dev/vda -- mkpart ESP fat32 1MB 512MB
parted /dev/vda -- set 2 esp on
# Format partitions
mkfs.ext4 -L nixos /dev/vda1 # Root partition with LABEL
mkfs.fat -F 32 -n boot /dev/vda2 # Boot partition with LABEL
# Create swap (optional, adjust size)
parted /dev/vda -- mkpart swap linux-swap 100% 8GB
mkswap -L swap /dev/vda3
swapon /dev/vda3
# Mount filesystems
mount /dev/disk/by-label/nixos /mnt
mkdir -p /mnt/boot
mount /dev/disk/by-label/boot /mnt/boot# Generate hardware config
nixos-generate-config --root /mnt
# Basic initial config (add vim for editing)
cat > /mnt/etc/nixos/configuration.nix <<'EOF'
{ config, pkgs, ... }:
{
imports = [ ./hardware-configuration.nix ];
# Bootloader
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# Hostname
networking.hostName = "nixos-utm";
networking.networkmanager.enable = true;
# Enable flakes & nix-command
nix.settings.experimental-features = [ "nix-command" "flakes" ];
# Timezone
time.timeZone = "America/New_York"; # Adjust
# Enable SSH
services.openssh.enable = true;
services.openssh.settings.PermitRootLogin = "yes"; # Temporary
# User account
users.users.sam = { # Change username
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" ];
# Temporary password, change after first login
initialPassword = "changeme";
};
# Packages for initial setup
environment.systemPackages = with pkgs; [
vim
git
curl
wget
htop
];
# Rosetta2 support (for x86_64 emulation)
virtualisation.rosetta.enable = true;
# Avahi for .local hostname discovery
services.avahi = {
enable = true;
nssmdns4 = true;
publish = {
enable = true;
addresses = true;
domain = true;
workstation = true;
};
};
system.stateVersion = "24.11"; # Match ISO version
}
EOF
# Review generated hardware config (uses by-label)
vim /mnt/etc/nixos/hardware-configuration.nix# Install
nixos-install
# Set root password when prompted
# Shutdown (don't restart yet)
shutdown -h now- In UTM settings, remove installation ISO from CD/DVD drive
- Start VM
# From macOS host, SSH into VM
ssh -p 2222 sam@localhost
# Change user password
passwd
# Generate SSH keys for this VM (for git, etc)
ssh-keygen -t ed25519 -C "sam@nixos-utm"
# Test internet connectivity
ping -c 3 nixos.org
# Verify .local hostname works
# From another terminal: ping nixos-utm.local# From macOS host:
# Copy SSH keys
scp -P 2222 ~/.ssh/id_ed25519 sam@localhost:~/.ssh/id_ed25519_host
scp -P 2222 ~/.ssh/id_ed25519.pub sam@localhost:~/.ssh/id_ed25519_host.pub
scp -P 2222 ~/.ssh/known_hosts sam@localhost:~/.ssh/
# Copy git config
scp -P 2222 ~/.gitconfig sam@localhost:~/
# Copy SSH config (if you have one)
scp -P 2222 ~/.ssh/config sam@localhost:~/.ssh/
# Inside VM: fix permissions
ssh -p 2222 sam@localhost
chmod 600 ~/.ssh/id_ed25519_host
chmod 644 ~/.ssh/id_ed25519_host.pub
chmod 600 ~/.ssh/config- In UTM VM settings → Sharing
- Add shared directory: Select macOS folder (e.g.,
~/shared-utm) - Inside VM:
# Mount shared directory
sudo mkdir -p /mnt/shared
sudo mount -t 9p -o trans=virtio share /mnt/shared
# Auto-mount on boot: add to /etc/fstab
# (We'll do this declaratively with NixOS config below)See "Advanced: Secrets Management" section below.
# Inside VM as user
cd ~
mkdir -p nix-config
cd nix-config
# Create flake.nix
cat > flake.nix <<'EOF'
{
description = "NixOS UTM VM Configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, home-manager, ... }: {
nixosConfigurations.nixos-utm = nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
modules = [
./configuration.nix
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.sam = import ./home.nix;
}
];
};
};
}
EOF
# Copy existing config as starting point
sudo cp /etc/nixos/configuration.nix ./configuration.nix
sudo cp /etc/nixos/hardware-configuration.nix ./hardware-configuration.nix
sudo chown sam:users *.nix
# Create home-manager config
cat > home.nix <<'EOF'
{ config, pkgs, ... }:
{
home.username = "sam";
home.homeDirectory = "/home/sam";
home.stateVersion = "24.11";
# Git configuration
programs.git = {
enable = true;
userName = "Your Name";
userEmail = "you@example.com";
extraConfig = {
init.defaultBranch = "main";
pull.rebase = true;
};
};
# SSH config
programs.ssh = {
enable = true;
matchBlocks = {
"github.com" = {
identityFile = "~/.ssh/id_ed25519";
};
};
};
# Bash/Shell config
programs.bash = {
enable = true;
shellAliases = {
ll = "ls -la";
".." = "cd ..";
};
};
# Packages for user environment
home.packages = with pkgs; [
ripgrep
fd
bat
eza
];
}
EOF
# Build and activate
sudo nixos-rebuild switch --flake .#nixos-utm
home-manager switch --flake .#nixos-utm# Add to configuration.nix:
{
# Auto-mount UTM shared directory
fileSystems."/mnt/shared" = {
device = "share";
fsType = "9p";
options = [
"trans=virtio"
"version=9p2000.L"
"rw"
"noauto"
"x-systemd.automount"
];
};
}# Initialize git repo
cd ~/nix-config
git init
git add .
git commit -m "init nixos utm config"
# Push to GitHub (optional)
gh repo create nixos-utm-config --private --source=. --pushIn UTM: Right-click VM → "Clone" → Name: nixos-utm-clean-install
export VM_NAME="my-nixos-vm"
nix run github:ciderale/nixos-utm#nixosCreate .#utm- Separate ARM64 (virtualized) & x86_64 (emulated) VMs
- SSH key distribution to root
- Test:
nix build --impurevalidates remote execution
For production setups, manage SSH keys & git configs declaratively using secrets management tools.
# Add to flake.nix inputs:
inputs.agenix.url = "github:ryantm/agenix";
# Add to modules:
modules = [
agenix.nixosModules.default
# or for home-manager:
# agenix.homeManagerModules.default
];mkdir -p secrets
cd secrets
# Create secrets.nix (defines who can decrypt)
cat > secrets.nix <<'EOF'
let
user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... user@host";
vm1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... root@nixos-utm";
in
{
"ssh-key.age".publicKeys = [ user1 vm1 ];
"gitconfig.age".publicKeys = [ user1 vm1 ];
}
EOF
# Encrypt secrets
nix run github:ryantm/agenix -- -e ssh-key.age
# (Paste your private key, save & exit)
nix run github:ryantm/agenix -- -e gitconfig.age
# (Paste gitconfig contents){ config, pkgs, ... }:
{
age.secrets.ssh-key = {
file = ../secrets/ssh-key.age;
path = "${config.home.homeDirectory}/.ssh/id_ed25519";
mode = "600";
};
age.secrets.gitconfig = {
file = ../secrets/gitconfig.age;
path = "${config.home.homeDirectory}/.gitconfig";
};
}# Add to flake.nix:
inputs.sops-nix.url = "github:Mic92/sops-nix";
# Install sops
nix-shell -p sopskeys:
- &user_sam age1abc...xyz # From: ssh-to-age -i ~/.ssh/id_ed25519
- &vm age1def...uvw # From: ssh-to-age -i /etc/ssh/ssh_host_ed25519_key.pub
creation_rules:
- path_regex: secrets/[^/]+\.yaml$
key_groups:
- age:
- *user_sam
- *vm# Create secrets file
sops secrets/default.yaml
# Edit in $EDITOR:
# ssh_key: |
# -----BEGIN OPENSSH PRIVATE KEY-----
# ...
# gitconfig: |
# [user]
# name = Sam{
sops.defaultSopsFile = ../secrets/default.yaml;
sops.age.sshKeyPaths = [ "/home/sam/.ssh/id_ed25519" ];
sops.secrets.ssh_key = {
owner = "sam";
path = "/home/sam/.ssh/id_ed25519_github";
};
}# In your nix-config repo:
nix-shell -p git-crypt
# Initialize
git-crypt init
# Add .gitattributes
echo "secrets/** filter=git-crypt diff=git-crypt" >> .gitattributes
# Store keys in secrets/
mkdir secrets
cp ~/.ssh/id_ed25519 secrets/
git add secrets/
git commit -m "add encrypted secrets"
# On new machine:
git-crypt unlock /path/to/key| Method | Complexity | Security | Use Case |
|---|---|---|---|
| SCP Transfer | Low | Manual | Quick setup, one-time |
| Shared Directory | Low | Host-dependent | Development, frequent changes |
| Git (plain) | Medium | Public only | Public dotfiles |
| Git-Crypt | Medium | Good | Simple encryption |
| agenix | Medium | Excellent | SSH-based, simple |
| sops-nix | High | Excellent | Complex secrets, teams |
- Initial setup: SCP transfer (Step 7, Option A)
- Development: UTM shared directory (Step 9)
- Production/Portable: agenix or sops-nix with flake-based config
- Use flakes for dependency locking
- Modular structure: separate hardware, base, VM-specific configs
- Version control all
.nixfiles - Label-based disk references (portability)
- Virtualization mode (ARM64) > Emulation mode (x86_64)
- Rosetta2 efficient for x86_64 when needed
- UTM Apple backend better battery life vs Docker Desktop
- ACPI Shutdown: Not supported - avoid host-side restart (recovery: console graphics + installer reattach + fsck)
- Network: Enable wireless networking for bare-metal to reach repos during rebuild
- SSH: Forward port 22 to local port for macOS access
- https://krisztianfekete.org/nixos-on-apple-silicon-with-utm/
- https://adrianhesketh.com/2024/04/20/setting-up-nixos-remote-builder-m1-mac/
- https://calcagno.blog/m1dev/
- https://context7.com/utmapp/utm
- https://github.com/mitchellh/nixos-config (vm-aarch64-utm configs)
- https://github.com/a-h/nixos (flake for aarch64 & x86_64)