This guide is written for AI agents. Every step is a copy-paste shell block that runs on a Linux workstation and talks to the Proxmox VE REST API with curl. Nothing has to be installed or run on the Proxmox host. The install is unattended (autoinstall user-data on a second CD-ROM labelled CIDATA), and the VM comes up with a static IP and key-based SSH.
Result
| Item | Value |
|---|---|
| OS | Ubuntu Server 26.04 LTS (latest point release; verified with 26.04.1) |
| VM | SeaBIOS, cpu=host, 2 vCPU, 4 GB RAM, 32 GB VirtIO SCSI disk (defaults; see Resources), VirtIO NIC |
| Network | Static IPv4 (netplan) set at install time |
| Login | $VM_USER with your SSH public key, passwordless sudo |
| Agent work | 6 command blocks and one wait loop |
Requirements
- Workstation:
bash,curl,jq,openssl,genisoimage(ormkisofs),ssh, and an SSH key pair (~/.ssh/id_ed25519.pub). - Proxmox VE 8.x/9.x reachable on port 8006. The examples log in as
root@pam. Another user needsVM.Allocate,VM.Config.*,VM.PowerMgmt,Datastore.AllocateSpace,Datastore.AllocateTemplateandSys.Modify(required bydownload-urlandsendkey). - A storage that accepts ISO images (
local) and one for VM disks (local-lvm). - A free IP address and VMID. The VM needs Internet access during the install.
Resources
| CPU | RAM | Disk | |
|---|---|---|---|
| Minimum (Ubuntu Server: system requirements) | amd64 | 1.5 GB (ISO install) | 5 GB |
| Suggested (same page) | 3 GB or more | 25 GB or more | |
| Defaults in this guide (verified) | 2 vCPU | 4 GiB | 32 GiB |
Canonical’s page states these figures for Ubuntu 24.04 LTS and notes that other releases might differ slightly; no separate figures were published for 26.04. The defaults meet the suggested values. With them, Step 5 to SSH login took about 10 minutes. To change them after installation, run pve PUT /nodes/$PVE_NODE/qemu/$VMID/config -d cores=4 -d memory=8192; the new values take effect after the VM is stopped and started again.
Step 1. Set variables
Edit the values, then run the block. All later blocks use these variables in the same shell.
PVE_HOST=192.168.1.13 # Proxmox VE host
PVE_NODE=pve # node name
PVE_USER=root@pam
PVE_PASS='your-root-password'
ISO_STORE=local # storage with "ISO image" content
DISK_STORE=local-lvm # storage for VM disks
BRIDGE=vmbr0
CORES=2 # vCPUs (see "Resources")
MEMORY=4096 # MiB
DISK_GB=32
VMID=807
VMNAME=ubuntu2604
IP=192.168.1.87
PREFIX=24
GW=192.168.1.1
DNS=8.8.8.8
VM_USER=agent
VM_PASS='ChangeMe-2026'
SSH_PUBKEY=$(cat ~/.ssh/id_ed25519.pub)
Step 2. Log in to the API
Creates a ticket (valid 2 hours) and two helpers: pve (API call) and wait_task (waits for a Proxmox task and prints its exit status, OK on success).
PVE=https://$PVE_HOST:8006/api2/json
read -r TICKET CSRF < <(curl -sk -d "username=$PVE_USER" --data-urlencode "password=$PVE_PASS" $PVE/access/ticket | jq -r '.data|"\(.ticket) \(.CSRFPreventionToken)"')
pve(){ local m=$1 p=$2; shift 2; curl -sSk -X "$m" -b "PVEAuthCookie=$TICKET" -H "CSRFPreventionToken: $CSRF" "$PVE$p" "$@"; }
wait_task(){ local u s; u=$(jq -rn --arg u "$1" '$u|@uri'); while sleep 5; do s=$(pve GET /nodes/$PVE_NODE/tasks/$u/status | jq -r '.data|"\(.status) \(.exitstatus)"'); [ "${s%% *}" = stopped ] && { echo "${s#* }"; return; }; done; }
pve GET /version | jq -c .data
Step 3. Download the ISO to Proxmox
Proxmox downloads the latest 26.04 live-server ISO itself and verifies its SHA-256. Skipped if the file already exists. Expected output: OK.
BASE=https://releases.ubuntu.com/26.04
read -r SHA ISO < <(curl -s $BASE/SHA256SUMS | grep live-server-amd64 | sed 's/ \*/ /' | sort -k2 -V | tail -1)
echo "$ISO"
pve GET "/nodes/$PVE_NODE/storage/$ISO_STORE/content?content=iso" | grep -q "iso/$ISO\"" && echo exists || \
wait_task "$(pve POST /nodes/$PVE_NODE/storage/$ISO_STORE/download-url -d content=iso -d filename=$ISO --data-urlencode url=$BASE/$ISO -d checksum=$SHA -d checksum-algorithm=sha256 | jq -r .data)"
Step 4. Create the autoinstall (CIDATA) ISO and upload it
The installer’s cloud-init reads user-data and meta-data from any volume labelled CIDATA. Expected output: OK.
mkdir -p /tmp/$VMNAME
HASH=$(openssl passwd -6 "$VM_PASS")
cat > /tmp/$VMNAME/user-data <<EOF
#cloud-config
autoinstall:
version: 1
locale: en_US.UTF-8
keyboard: {layout: us}
timezone: Etc/UTC
network:
version: 2
ethernets:
nic0:
match: {name: "en*"}
addresses: [$IP/$PREFIX]
routes: [{to: default, via: $GW}]
nameservers: {addresses: [$DNS]}
storage: {layout: {name: lvm}}
identity: {hostname: $VMNAME, username: $VM_USER, password: "$HASH"}
ssh: {install-server: true, allow-pw: false, authorized-keys: ["$SSH_PUBKEY"]}
late-commands:
- echo '$VM_USER ALL=(ALL) NOPASSWD:ALL' > /target/etc/sudoers.d/$VM_USER
shutdown: reboot
EOF
echo "instance-id: $VMNAME" > /tmp/$VMNAME/meta-data
genisoimage -quiet -V CIDATA -J -r -o /tmp/$VMNAME/$VMNAME-cidata.iso /tmp/$VMNAME/user-data /tmp/$VMNAME/meta-data
wait_task "$(pve POST /nodes/$PVE_NODE/storage/$ISO_STORE/upload -F content=iso -F filename=@/tmp/$VMNAME/$VMNAME-cidata.iso | jq -r .data)"
Step 5. Create and start the VM, add autoinstall to the boot line
The boot order puts the empty disk first, so the VM boots the ISO now and the installed disk after the install reboots.
If autoinstall is not on the kernel command line, the installer stops at Continue with autoinstall? (yes|no). The second command adds it through the GRUB editor with the sendkey API: e opens the entry, three down moves reach the linux /casper/vmlinuz --- line, and ctrl-x boots. The GRUB menu appears about 1 second after power-on and waits 30 seconds, so the 10-second delay is safe. Expected output: OK.
wait_task "$(pve POST /nodes/$PVE_NODE/qemu -d vmid=$VMID -d name=$VMNAME -d ostype=l26 -d cpu=host -d cores=$CORES -d memory=$MEMORY \
-d scsihw=virtio-scsi-single --data-urlencode "scsi0=$DISK_STORE:$DISK_GB" --data-urlencode "net0=virtio,bridge=$BRIDGE" \
--data-urlencode "ide2=$ISO_STORE:iso/$ISO,media=cdrom" --data-urlencode "ide3=$ISO_STORE:iso/$VMNAME-cidata.iso,media=cdrom" \
--data-urlencode "boot=order=scsi0;ide2" -d start=1 | jq -r .data)"
sleep 10; for k in e down down down end spc a u t o i n s t a l l ctrl-x; do pve PUT /nodes/$PVE_NODE/qemu/$VMID/sendkey -d key=$k >/dev/null; sleep 0.2; done
Step 6. Wait for SSH and log in
The live installer also runs an SSH server, so the loop waits for a successful key login rather than an open port 22. Verified time from Step 5 to login: about 10 minutes (the installer also applies security updates, so it varies with the network).
SSHO="-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
until ssh $SSHO $VM_USER@$IP true 2>/dev/null; do sleep 20; done
ssh $SSHO $VM_USER@$IP 'grep PRETTY /etc/os-release; ip -4 -br addr; sudo -n true && echo sudo-ok'
Step 7. Clean up (optional)
Detach both CD-ROMs and delete the CIDATA ISO from storage.
pve PUT /nodes/$PVE_NODE/qemu/$VMID/config -d delete=ide3 --data-urlencode "ide2=none,media=cdrom"
pve DELETE /nodes/$PVE_NODE/storage/$ISO_STORE/content/$ISO_STORE:iso/$VMNAME-cidata.iso
Troubleshooting
wait_taskprints an error instead ofOK: list the latest failed tasks and their status text withpve GET "/nodes/$PVE_NODE/tasks?errors=1" | jq -r '.data[:3][] | "\(.type) \(.id) \(.status)"'.Permission check failedon create: a non-root user must create the VM in a pool it has rights on (add-d pool=<pool>).- SSH never opens after 30 minutes: open the VM console in the Proxmox web UI. A YAML error in
user-datamakes the installer fall back to the interactive UI. - To start over:
pve POST /nodes/$PVE_NODE/qemu/$VMID/status/stop, thenpve DELETE "/nodes/$PVE_NODE/qemu/$VMID?purge=1&destroy-unreferenced-disks=1".