Background

I am happily using Proxmox’s built in support for Cloud-Init, which lets me configure and re-configure users, SSH keys and network settings of my VMs.

I want to be even lazier, though. With one or two clicks, I want to be able to spin up a VM that:

  • Has Tailscale installed and logged in.
  • Is already running the QEMU guest agent.
  • Has Docker preinstalled.
  • Can still be reconfigured using the Cloud-Init UI options.

After some tinkering, I successfully got this set up.

Instructions

Techno Tim on YouTube already has a fantastic video covering a basic cloud-init setup in Proxmox. His instructions are great, so I won’t repeat them. Follow his video until around 6:40, pause, and come back here.

At this point, you should have a VM and some configured Cloud-Init defaults like passwords, SSH keys, network setup etc. You should not have started your VM yet, and you should not have turned it into a template.

To do more advanced things with cloud-init, like pre-installing packages, we’ll need to create a custom config file and point our VM at it:

1. Enable the QEMU Guest Agent for the VM. This can be done either under Options in the UI or by running shqm set 9000 --agent 1 in your host shell.

2. Increase the size of the VM disk in the Hardware options for your VM. This is to avoid running out of space when we pre-install Docker on the first boot. I tacked on an extra 10 GB.

3. Write and save your custom cloud-init config file. You’ll need to save it to the snippet storage on your PVE host. In my case that’s /var/lib/vz/snippets/. My file (which I’ve called vendorconfig.yaml) looks like this:

#cloud-config
runcmd:
  # Install Qemu guest agent and start it
  - ['apt', 'install', '-y', 'qemu-guest-agent']
  - ['systemctl', 'start', 'qemu-guest-agent']
  # Install Docker
  - ['sh', '-c', 'curl -fsSL https://get.docker.com | sh']
  # Install Tailscale and enable ipv4-forwarding
  - ['sh', '-c', 'curl -fsSL https://tailscale.com/install.sh | sh']
  - ['sh', '-c', "echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf && echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf && sudo sysctl -p /etc/sysctl.d/99-tailscale.conf" ]
  # Replace [your-ts-authkey] with one generated in your Tailscale settings.
  # 
  # Note: If you create a key that requires new machines to be approved, the 
  # `tailscale up` command will wait for approval. So keep it here at the 
  # end of your file so that it doesn't prevent other commands from running.
  - ['tailscale', 'up', '--auth-key=[your-ts-authkey]', '--ssh']

I am only using the runcmd module here, but cloud-init allows you to do a ton of stuff, including running Ansible for more complex tasks.

Point your VM at your new custom config file. This is the key step, and can’t be done in the Proxmox UI yet. So fire up a shell on your PVE host and run:

qm set 9000 --cicustom "vendor=local:snippets/vendorconfig.yaml"

Replace 9000 with the ID of your VM, obviously.

(Optional) Make a backup of your “pre template” VM. This can come in handy in case you want to reconfigure it later, since templates are immutable.

Now, go back and finish Tim’s fantastic video. When you create a clone VM and start it, you’ll see Docker and Tailscale being installed automatically. After a minute or two, your VM should pop up on the Tailscale admin panel.

Notes

The config file does not get baked into your VM

The config file is referred to by your VMs config in Proxmox, but is not “baked” into it in any way. If you delete that file, your VM will stop working. This also means that you must copy it to all of your PVE hosts if you are running a cluster and want to migrate your new VMs or templates to other cluster hosts.

This also means that we can make changes to the config file without having to re-build and re-templatize a VM. For example, when my Tailscale auth token expires I can just go in and replace it.

Note, however, that the many cloud-init commands (including runcmd ) only runs on first boot. To force them to run on existing machines, you run cloud-init clean and then reboot.

Why attach the config as vendor?

Proxmox allows us to append many kinds of cloud-init config files to our VM. In this case, I’m attaching a vendor config. Why? Because if I attach a user config file, it will replace the value that are set using the Cloud-Init menu in the Proxmox UI. That’s annoying and would require a unique config file for each VM I spin up. No go.

By injecting our config as a vendor config file, we can continue to use the UI for some configuration but still harness the full power of cloud-init in our config file.

Big thanks for the folks in this thread on the Proxmox forums for showing how this works.