Skip to main content

        A practical guide to enabling cloud-init in Proxmox LXC containers and automating provisioning with Terraform.

Cloud-Init for Proxmox LXC Containers

A practical guide to enabling cloud-init in Proxmox LXC containers and automating provisioning with Terraform.

Proxmox VE has excellent cloud-init support for virtual machines, but LXC containers are a different story. There’s no native cloud-init integration for containers, which makes automated provisioning challenging. While working on my homelab infrastructure, I found a workaround: pre-baking cloud-init configuration directly into LXC templates.

This approach lets you use the same cloud-init workflows you’d use with VMs-user creation, SSH keys, package installation, custom scripts but for containers.

How It Works

The solution is straightforward. LXC templates from Linux Containers are just compressed tarballs containing a root filesystem. Many of these images come with cloud-init pre-installed. We can:

  1. Download a cloud-ready template
  2. Inject our cloud-init configuration files
  3. Remove the disable flag (if present)
  4. Recompress and use as a new template

Cloud-init uses the NoCloud datasource when it finds seed files at boot. We need two files inside the container’s filesystem:

  • /var/lib/cloud/seed/nocloud/user-data - Cloud-init configuration (users, packages, scripts)
  • /var/lib/cloud/seed/nocloud/meta-data - Instance metadata (hostname, instance-id)

Additionally, we must ensure /etc/cloud/cloud-init.disabled doesn’t exist, as this file prevents cloud-init from running.

The Repacking Script

I wrote a bash script lxcc.sh to automate the template repacking process:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/usr/bin/env bash
set -e

if [ "$#" -ne 4 ]; then
  echo "Usage: $0 <input.tar.xz> <user-data> <meta-data> <output.tar.xz>"
  exit 1
fi

INPUT="$1"
USER_DATA="$2"
META_DATA="$3"
OUTPUT="$4"

# Select tar implementation (GNU tar required)
if [[ "$(uname)" == "Darwin" ]]; then
  TAR="gtar"
else
  TAR="tar"
fi

if ! $TAR --version 2>/dev/null | grep -q "GNU tar"; then
  echo "GNU tar required. On macOS: brew install gnu-tar"
  exit 1
fi

TMP_TAR="$(mktemp).tar"

# Decompress
xz -dc "$INPUT" > "$TMP_TAR"

# Remove cloud-init.disabled if present
if $TAR -tf "$TMP_TAR" | grep -q '^./etc/cloud/cloud-init.disabled$'; then
  $TAR --delete -f "$TMP_TAR" ./etc/cloud/cloud-init.disabled
fi

# Inject user-data and meta-data
$TAR --owner=0 --group=0 \
  --transform='s|^.*$|var/lib/cloud/seed/nocloud/user-data|' \
  -rf "$TMP_TAR" "$USER_DATA"

$TAR --owner=0 --group=0 \
  --transform='s|^.*$|var/lib/cloud/seed/nocloud/meta-data|' \
  -rf "$TMP_TAR" "$META_DATA"

# Recompress and cleanup
xz -z -c "$TMP_TAR" > "$OUTPUT"
rm -f "$TMP_TAR"

echo "Created: $OUTPUT"

Note: On macOS, you’ll need GNU tar (brew install gnu-tar) since BSD tar doesn’t support the --transform and --delete options.

Usage

1
./lxcc.sh ubuntu-noble-cloud.tar.xz user-data.yml meta-data.yml output.tar.xz

Arguments:

  • input.tar.xz - Original template from linuxcontainers.org
  • user-data - Your cloud-init configuration
  • meta-data - Instance metadata
  • output.tar.xz - Repacked template ready for Proxmox

Example Cloud-Init Configuration

Here’s a practical user-data example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#cloud-config
hostname: my-container
timezone: Europe/London

# Security
ssh_pwauth: false
disable_root: true

users:
  - name: admin
    groups: [sudo]
    shell: /bin/bash
    lock_passwd: true
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... your-key-here

packages:
  - openssh-server
  - curl
  - htop
  - vim

# Configure firewall
runcmd:
  - ufw allow from 192.168.1.0/24 to any port 22 proto tcp
  - ufw --force enable

And a minimal meta-data:

1
2
instance-id: my-container
local-hostname: my-container

After repacking, import the template to Proxmox and create containers from it. Cloud-init runs automatically on first boot.

Terraform Integration

You can automate template preparation with Terraform using local-exec provisioners:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
variable "container_name" {
  default = "web-server"
}

locals {
  template_dir = "${path.root}/.terraform/lxc-templates"
}

resource "local_file" "user_data" {
  filename = "${local.template_dir}/${var.container_name}-user-data"
  content  = <<-EOT
    #cloud-config
    hostname: ${var.container_name}
    users:
      - name: admin
        groups: [sudo]
        shell: /bin/bash
        lock_passwd: true
  EOT
}

resource "local_file" "meta_data" {
  filename = "${local.template_dir}/${var.container_name}-meta-data"
  content  = "instance-id: ${var.container_name}\nlocal-hostname: ${var.container_name}\n"
}

resource "terraform_data" "repack_template" {
  triggers_replace = [
    local_file.user_data.content,
  ]

  provisioner "local-exec" {
    command = <<-EOT
      mkdir -p "${local.template_dir}"

      # Download base template if not cached
      BASE_TEMPLATE="${local.template_dir}/ubuntu-noble-cloud.tar.xz"
      if [ ! -f "$BASE_TEMPLATE" ]; then
        curl -fSL -o "$BASE_TEMPLATE" \
          "https://images.linuxcontainers.org/images/ubuntu/noble/amd64/cloud/20260212_07%3A42/rootfs.tar.xz"
      fi

      # Repack with cloud-init configuration
      ./lxcc.sh "$BASE_TEMPLATE" \
        "${local_file.user_data.filename}" \
        "${local_file.meta_data.filename}" \
        "${local.template_dir}/${var.container_name}-cloudinit.tar.xz"
    EOT
  }

  depends_on = [local_file.user_data, local_file.meta_data]
}

# Create LXC container from repacked template
resource "proxmox_lxc" "web_server" {
  # ...
  template = "${local.template_dir}/${var.container_name}-cloudinit.tar.xz"
  # ...
}

The full implementation is available in my homelab repository . Feel free to adapt it for your own infrastructure.