Create an Ubuntu Server 24.04 LTS VM on Proxmox VE (CLI only: ISO download → unattended install → SSH)

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

ItemValue
OSUbuntu Server 24.04 LTS (latest point release; verified with 24.04.5)
VMSeaBIOS, cpu=host, 2 vCPU, 4 GB RAM, 32 GB VirtIO SCSI disk (defaults; see Resources), VirtIO NIC
NetworkStatic IPv4 (netplan) set at install time
Login$VM_USER with your SSH public key, passwordless sudo
Agent work6 command blocks and one wait loop

Requirements

  • Workstation: bash, curl, jq, openssl, genisoimage (or mkisofs), 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 needs VM.Allocate, VM.Config.*, VM.PowerMgmt, Datastore.AllocateSpace, Datastore.AllocateTemplate and Sys.Modify (required by download-url and sendkey).
  • 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

CPURAMDisk
Minimum (Ubuntu Server: system requirements)amd641.5 GB (ISO install)5 GB
Suggested (same page)3 GB or more25 GB or more
Defaults in this guide (verified)2 vCPU4 GiB32 GiB

The defaults meet Canonical’s suggested values. With them, Step 5 to SSH login took about 7 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=806
VMNAME=ubuntu2404
IP=192.168.1.86
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 24.04 live-server ISO itself and verifies its SHA-256. Skipped if the file already exists. Expected output: OK.

BASE=https://releases.ubuntu.com/24.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 7 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_task prints an error instead of OK: list the latest failed tasks and their status text with pve GET "/nodes/$PVE_NODE/tasks?errors=1" | jq -r '.data[:3][] | "\(.type) \(.id) \(.status)"'.
  • Permission check failed on 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-data makes the installer fall back to the interactive UI.
  • To start over: pve POST /nodes/$PVE_NODE/qemu/$VMID/status/stop, then pve DELETE "/nodes/$PVE_NODE/qemu/$VMID?purge=1&destroy-unreferenced-disks=1".
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次