|
| 1 | +--- |
| 2 | +display_name: Proxmox VM |
| 3 | +description: Provision VMs on Proxmox VE as Coder workspaces |
| 4 | +icon: ../../../../.icons/proxmox.svg |
| 5 | +verified: false |
| 6 | +tags: [proxmox, vm, cloud-init, qemu] |
| 7 | +--- |
| 8 | + |
| 9 | +# Proxmox VM Template for Coder |
| 10 | + |
| 11 | +Provision Linux VMs on Proxmox as [Coder workspaces](https://coder.com/docs/workspaces). The template clones a cloud‑init base image, injects user‑data via Snippets, and runs the Coder agent under the workspace owner's Linux user. |
| 12 | + |
| 13 | +## Prerequisites |
| 14 | + |
| 15 | +- Proxmox VE 8/9 |
| 16 | +- Proxmox API token with access to nodes and storages |
| 17 | +- SSH access from Coder provisioner to Proxmox VE |
| 18 | +- Storage with "Snippets" content enabled |
| 19 | +- Ubuntu cloud‑init image/template on Proxmox |
| 20 | + - Latest images: https://cloud-images.ubuntu.com/ ([source](https://cloud-images.ubuntu.com/)) |
| 21 | + |
| 22 | +## Prepare a Proxmox Cloud‑Init Template (once) |
| 23 | + |
| 24 | +Run on the Proxmox node. This uses a RELEASE variable so you always pull a current image. |
| 25 | + |
| 26 | +```bash |
| 27 | +# Choose a release (e.g., jammy or noble) |
| 28 | +RELEASE=jammy |
| 29 | +IMG_URL="https://cloud-images.ubuntu.com/${RELEASE}/current/${RELEASE}-server-cloudimg-amd64.img" |
| 30 | +IMG_PATH="/var/lib/vz/template/iso/${RELEASE}-server-cloudimg-amd64.img" |
| 31 | + |
| 32 | +# Download cloud image |
| 33 | +wget "$IMG_URL" -O "$IMG_PATH" |
| 34 | + |
| 35 | +# Create base VM (example ID 999), enable QGA, correct boot order |
| 36 | +NAME="ubuntu-${RELEASE}-cloudinit" |
| 37 | +qm create 999 --name "$NAME" --memory 4096 --cores 2 \ |
| 38 | + --net0 virtio,bridge=vmbr0 --agent enabled=1 |
| 39 | +qm set 999 --scsihw virtio-scsi-pci |
| 40 | +qm importdisk 999 "$IMG_PATH" local-lvm |
| 41 | +qm set 999 --scsi0 local-lvm:vm-999-disk-0 |
| 42 | +qm set 999 --ide2 local-lvm:cloudinit |
| 43 | +qm set 999 --serial0 socket --vga serial0 |
| 44 | +qm set 999 --boot 'order=scsi0;ide2;net0' |
| 45 | + |
| 46 | +# Enable Snippets on storage 'local' (one‑time) |
| 47 | +pvesm set local --content snippets,vztmpl,backup,iso |
| 48 | + |
| 49 | +# Convert to template |
| 50 | +qm template 999 |
| 51 | +``` |
| 52 | + |
| 53 | +Verify: |
| 54 | + |
| 55 | +```bash |
| 56 | +qm config 999 | grep -E 'template:|agent:|boot:|ide2:|scsi0:' |
| 57 | +``` |
| 58 | + |
| 59 | +### Enable Snippets via GUI |
| 60 | + |
| 61 | +- Datacenter → Storage → select `local` → Edit → Content → check "Snippets" → OK |
| 62 | +- Ensure `/var/lib/vz/snippets/` exists on the node for snippet files |
| 63 | +- Template page → Cloud‑Init → Snippet Storage: `local` → File: your yml → Apply |
| 64 | + |
| 65 | +## Configure this template |
| 66 | + |
| 67 | +Edit `terraform.tfvars` with your environment: |
| 68 | + |
| 69 | +```hcl |
| 70 | +# Proxmox API |
| 71 | +proxmox_api_url = "https://<PVE_HOST>:8006/api2/json" |
| 72 | +proxmox_api_token_id = "<USER@REALM>!<TOKEN>" |
| 73 | +proxmox_api_token_secret = "<SECRET>" |
| 74 | +
|
| 75 | +# SSH to the node (for snippet upload) |
| 76 | +proxmox_host = "<PVE_HOST>" |
| 77 | +proxmox_password = "<NODE_ROOT_OR_SUDO_PASSWORD>" |
| 78 | +proxmox_ssh_user = "root" |
| 79 | +
|
| 80 | +# Infra defaults |
| 81 | +proxmox_node = "pve" |
| 82 | +disk_storage = "local-lvm" |
| 83 | +snippet_storage = "local" |
| 84 | +bridge = "vmbr0" |
| 85 | +vlan = 0 |
| 86 | +clone_template_vmid = 999 |
| 87 | +``` |
| 88 | + |
| 89 | +### Variables (terraform.tfvars) |
| 90 | + |
| 91 | +- These values are standard Terraform variables that the template reads at apply time. |
| 92 | +- Place secrets (e.g., `proxmox_api_token_secret`, `proxmox_password`) in `terraform.tfvars` or inject with environment variables using `TF_VAR_*` (e.g., `TF_VAR_proxmox_api_token_secret`). |
| 93 | +- You can also override with `-var`/`-var-file` if you run Terraform directly. With Coder, the repo's `terraform.tfvars` is bundled when pushing the template. |
| 94 | + |
| 95 | +Variables expected: |
| 96 | + |
| 97 | +- `proxmox_api_url`, `proxmox_api_token_id`, `proxmox_api_token_secret` (sensitive) |
| 98 | +- `proxmox_host`, `proxmox_password` (sensitive), `proxmox_ssh_user` |
| 99 | +- `proxmox_node`, `disk_storage`, `snippet_storage`, `bridge`, `vlan`, `clone_template_vmid` |
| 100 | +- Coder parameters: `cpu_cores`, `memory_mb`, `disk_size_gb` |
| 101 | + |
| 102 | +## Proxmox API Token (GUI/CLI) |
| 103 | + |
| 104 | +Docs: https://pve.proxmox.com/wiki/User_Management#pveum_tokens |
| 105 | + |
| 106 | +GUI: |
| 107 | + |
| 108 | +1. (Optional) Create automation user: Datacenter → Permissions → Users → Add (e.g., `terraform@pve`) |
| 109 | +2. Permissions: Datacenter → Permissions → Add → User Permission |
| 110 | + - Path: `/` (or narrower covering your nodes/storages) |
| 111 | + - Role: `PVEVMAdmin` + `PVEStorageAdmin` (or `PVEAdmin` for simplicity) |
| 112 | +3. Token: Datacenter → Permissions → API Tokens → Add → copy Token ID and Secret |
| 113 | +4. Test: |
| 114 | + |
| 115 | +```bash |
| 116 | +curl -k -H "Authorization: PVEAPIToken=<USER@REALM>!<TOKEN>=<SECRET>" \ |
| 117 | + https:// < PVE_HOST > :8006/api2/json/version |
| 118 | +``` |
| 119 | + |
| 120 | +CLI: |
| 121 | + |
| 122 | +```bash |
| 123 | +pveum user add terraform@pve --comment 'Terraform automation user' |
| 124 | +pveum aclmod / -user terraform@pve -role PVEAdmin |
| 125 | +pveum user token add terraform@pve terraform --privsep 0 |
| 126 | +``` |
| 127 | + |
| 128 | +## Use |
| 129 | + |
| 130 | +```bash |
| 131 | +# From this directory |
| 132 | +coder templates push --yes proxmox-cloudinit --directory . | cat |
| 133 | +``` |
| 134 | + |
| 135 | +Create a workspace from the template in the Coder UI. First boot usually takes 60–120s while cloud‑init runs. |
| 136 | + |
| 137 | +## How it works |
| 138 | + |
| 139 | +- Uploads rendered cloud‑init user‑data to `<storage>:snippets/<vm>.yml` via the provider's `proxmox_virtual_environment_file` |
| 140 | +- VM config: `virtio-scsi-pci`, boot order `scsi0, ide2, net0`, QGA enabled |
| 141 | +- Linux user equals Coder workspace owner (sanitized). To avoid collisions, reserved names (`admin`, `root`, etc.) get a suffix (e.g., `admin1`). User is created with `primary_group: adm`, `groups: [sudo]`, `no_user_group: true` |
| 142 | +- systemd service runs as that user: |
| 143 | + - `coder-agent.service` |
| 144 | + |
| 145 | +## Troubleshooting quick hits |
| 146 | + |
| 147 | +- iPXE boot loop: ensure template has bootable root disk and boot order `scsi0,ide2,net0` |
| 148 | +- QGA not responding: install/enable QGA in template; allow 60–120s on first boot |
| 149 | +- Snippet upload errors: storage must include `Snippets`; token needs Datastore permissions; path format `<storage>:snippets/<file>` handled by provider |
| 150 | +- Permissions errors: ensure the token's role covers the target node(s) and storages |
| 151 | +- Verify snippet/QGA: `qm config <vmid> | egrep 'cicustom|ide2|ciuser'` |
| 152 | + |
| 153 | +## References |
| 154 | + |
| 155 | +- Ubuntu Cloud Images (latest): https://cloud-images.ubuntu.com/ ([source](https://cloud-images.ubuntu.com/)) |
| 156 | +- Proxmox qm(1) manual: https://pve.proxmox.com/pve-docs/qm.1.html |
| 157 | +- Proxmox Cloud‑Init Support: https://pve.proxmox.com/wiki/Cloud-Init_Support |
| 158 | +- Terraform Proxmox provider (bpg): `bpg/proxmox` on the Terraform Registry |
| 159 | +- Coder – Best practices & templates: |
| 160 | + - https://coder.com/docs/tutorials/best-practices/speed-up-templates |
| 161 | + - https://coder.com/docs/tutorials/template-from-scratch |
0 commit comments