Simulating cloud-init Behavior in Proxmox LXC Containers

The Proxmox default templates are great, but are (as of: June 2025) a bit dated, requiring ~150 updates. For many of my VMs I use recent releases to minimize the repetitive application of updates before each is usable. This guide extends that practice to LXC containers.

🧱 1. Download a Cloud-Ready Template

Download a recent, cloud-ready image from https://images.linuxcontainers.org/ like ubuntu noble amd64 cloud into Proxmox-VE/local/CT Templates, Download from URL.

For the specific URL, you're looking for the rootfs.tar.xz - https://images.linuxcontainers.org/images/ubuntu/noble/amd64/cloud/20250620_07:42/rootfs.tar.xz

And give it a Proxmox-VE friendly name like ubuntu-24.04-latest_24.04-2_amd64.tar.xz

Something like update_cloud_image.sh to download the latest:

#!/usr/bin/env bash
BASEURL=https://images.linuxcontainers.org/images/ubuntu/noble/amd64/cloud/
FILECOMP=rootfs.tar.xz
TARGET=ubuntu-24.04-latest_24.04-2_amd64.tar.xz
DIRCOMP=$(wget -qO- https://images.linuxcontainers.org/images/ubuntu/noble/amd64/cloud/ | \
               lynx -dump -listonly -nonumbers -stdin | tail -n 1 | awk -F / '{print $6}')
echo $DIRCOMP > current_container
if cmp -s current_container latest_container; then
   echo "Container image is current"
else
   echo "New image available. Fetching now."
   rm -f /var/lib/vz/template/cache/$TARGET
   wget https://images.linuxcontainers.org/images/ubuntu/noble/amd64/cloud/$DIRCOMP/$FILECOMP -O \
        /var/lib/vz/template/cache/$TARGET
   cp current_container latest_container
fi

Or, just use the fetch_container_image role!

🛠️ 2. Create (But Don’t Start) Your Container

Create your container through the gui, or the command line:

pct create 105 local:vztmpl/ubuntu-24.04-latest_24.04-2_amd64.tar.xz \
               --hostname piglet \
               --cores 1 --memory 2048 \
               --net0 name=eth0,bridge=vmbr0,ip=192.168.86.7/24,gw=192.168.86.1 \
               --rootfs local:8 \
               --unprivileged 1

Notice we're not starting it on creation!

🔧 3. Inject cloud-init Files into the RootFS

So before you start it do the following:

Mount the rootfs with:

pct mount 105

And you'll find the rootfs mounted at /var/lib/lxc/105/rootfs !

Create a meta-data file like:

instance-id: ubuntu-lxc-2404
local-hostname: piglet
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: false
      addresses:
        - 192.168.86.7/24
      gateway4: 192.168.86.1
      nameservers:
        addresses:
          - 192.168.86.1
          - 8.8.8.8

and a user-data file like:

#cloud-config
hostname: piglet
manage_etc_hosts: true
users:
  - default
  - name: ubuntu
    groups: [adm, cdrom, dip, lxd, sudo]
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    lock_passwd: true
    ssh_authorized_keys:
      - ssh-rsa AAAA...your_key_here...
package_update: true
package_upgrade: true
packages:
  - openssh-server

and a 90_nocloud.cfg file like:

datasource:
  NoCloud:
    seedfrom: file:///var/lib/cloud/seed/nocloud-net/

Then create the cloud data diretory and copy some files!

mkdir -p /var/lib/lxc/105/rootfs/var/lib/cloud/seed/nocloud-net/
cp meta-data /var/lib/lxc/105/rootfs/var/lib/cloud/seed/nocloud-net/meta-data
cp user-data /var/lib/lxc/105/rootfs/var/lib/cloud/seed/nocloud-net/user-data
cp 90_nocloud.cfg /var/lib/lxc/105/rootfs/etc/cloud/cloud.cfg.d/90_nocloud.cfg

⚠️ 4. Tame Unprivileged Container Quirks

Now for a couple of non-obvious things:

chown -R 100000:100000 /var/lib/lxc/105/rootfs/var/lib/cloud
rm /var/lib/lxc/105/rootfs/etc/cloud/cloud-init.disabled

🚀 5. Launch and Verify

Now, feel free to:

pct start 105

and then ssh in as ubuntu!

ssh ubuntu@192.168.86.7

Cloud-init should have set the hostname, installed OpenSSH, and dropped in your key.

Have fun!

💡Bonus Suggestion: Wrap it up with Reusability

Want to clone this container setup for future projects? Use pct clone or turn it into a template once it’s working.