proxmox

Version 11.7 by Kevin Wiki on 2026/05/18 15:42

Getting Started with Proxmox VE LXC and VM Templates

Proxmox VE (PVE) allows users to create and manage both LXC containers and KVM virtual machines (VMs). This guide walks you through the process of downloading, importing, and creating templates for both.

LXC Templates

LXC containers are lightweight and ideal for running Linux services with minimal overhead.

List Available Templates

To view the available LXC templates:

pveam list

Download Templates

Use the pveam download command to import templates to the local storage:

pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.gz
pveam download local ubuntu-24.04-standard_24.04-1_amd64.tar.zst
pveam download local debian-12-standard_11.7-1_amd64.tar.zst

Once downloaded, these templates can be used to create new LXC containers from the Proxmox web interface or via CLI.

VM Template from Ubuntu Cloud Image

KVM VMs are ideal when you need full virtualization, for instance, to run Windows or more complex Linux systems.

Download Ubuntu Cloud Image

Download the official Ubuntu 24.04 cloud filename

wget http://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img

Create the Virtual Machine

Create a new VM with ID 910 (you can pick any unused ID):

qm create 910 -name template-ubuntu-jammy -memory 2048 -net0 virtio,bridge=vmbr0 -cores 2 -sockets 1

Import and Attach the Disk

Choose the correct storage (replace nvme if you use a different storage name):

qm importdisk 910 ubuntu-24.04-server-cloudimg-amd64.img nvme
qm set 910 -scsihw virtio-scsi-pci -virtio0 nvme:vm-910-disk-0

Configure the VM

qm set 910 -serial0 socket
qm set 910 -boot c -bootdisk virtio0
qm set 910 -agent 1
qm set 910 -hotplug disk,network,usb
qm set 910 -vcpus 1
qm set 910 -vga qxl
qm set 910 -ide2 nvme:cloudinit
qm resize 910 virtio0 +8G

If your disk is using SCSI instead of virtio, resize like this:

qm resize 910 scsi0 +8G

Convert the VM into a Template

qm template 910

Now you can use this template to clone new VMs instantly.

Bash Script to Automate Setup

Install the above using bash script below

setup_proxmox_templates.sh

#!/bin/bash

# Exit on errors
set -e

echo "Downloading LXC templates..."
pveam download nvme ubuntu-22.04-standard_22.04-1_amd64.tar.zst
pveam download nvme ubuntu-24.04-standard_24.04-2_amd64.tar.zst
pveam download nvme alpine-3.21-default_20241217_amd64.tar.xz
pveam download nvme debian-12-standard_12.7-1_amd64.tar.zst

echo "Downloading Ubuntu cloud image..."
wget -N http://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img

echo "Creating VM Template..."
qm create 910 -name template-ubuntu-jammy -memory 2048 -net0 virtio,bridge=vmbr0 -cores 2 -sockets 1 && 1

# qm importdisk 910 ubuntu-24.04-server-cloudimg-amd64.img nvme
# qm set 910 -scsihw virtio-scsi-pci -scsi0-virtio0 nvme:910/vm-910-disk-0
qm set 910 -scsihw virtio-scsi-pci -scsi0 nvme:0,import-from=/mnt/nvmestorage/template/iso/ubuntu-24.04-server-cloudimg-amd64.img

qm set 910 -serial0 socket
qm set 910 -boot c -bootdisk virtio0
qm set 910 -agent 1
qm set 910 -hotplug disk,network,usb
qm set 910 -vcpus 1
qm set 910 -vga qxl
qm set 910 -ide2 nvme:cloudinit
# qm resize 910 scsi0 +8G

read -p "Confirm converting to template by pressing Enter"
qm template 910

echo "Templates setup complete."

VM runtime setup

After creating the VM and before making it into a template there are some programs and settings we want to ensure exists always.

clear bash history to not leave any configuration in history, clear and disable history file before proceeding:

unset HISTFILE
export HISTSIZE=0
export HISTFILESIZE=0

sudo rm /.bash_history
rm ~/.bash_history

qemu-guest-agent is for allowing proxmox to query information from the VM such as IP address, shutdown commands, etc

sudo apt update
sudo apt upgrade -y
sudo apt install qemu-guest-agent -y

sudo systemctl enable qemu-guest-agent.service
sudo systemctl start qemu-guest-agent.service

reset machine-id to not have overlapping ids from same template

cat /dev/null > /etc/machine-id
cat /dev/null > /var/lib/dbus/machine-id

cloud-init is a great hook for installing or configuring programs or receiving variables from cloudinit CDROM drive. Making it easier to change IP, hostname, DNS, username/password, etc between VMs.

If you used a cloud-init base image it will have run the default cloudinit which installs and configures a bunch of systems. To clean this up run:
```
```
Reset cloud-init to run all init and modules at next boot:

cloud-init clean

This is a debian example of what we are looking for:

# The top level settings are used as module
# and system configuration.
# A set of users which may be applied and/or used by various modules
# when a 'default' entry is found it will reference the 'default_user'
# from the distro configuration specified below

# If this is set, 'root' will not be able to ssh in and they
# will get a message to login instead as the default $user
disable_root: true

# This will cause the set+update hostname module to not operate (if true)
preserve_hostname: false

apt:
  # This prevents cloud-init from rewriting apt's sources.list file,
  # which has been a source of surprise.
  preserve_sources_list: true

# manually managed resolv
manage_resolv_conf: false

package_update: true
packages:
  - qemu-guest-agent

# The modules that run in the 'init' stage
cloud_init_modules:
 - seed_random
 - bootcmd
 - write-files
 - growpart
 - resizefs
 - disk_setup
 - mounts
 - set_hostname
 - update_hostname
 - update_etc_hosts
 - ca-certs
 - rsyslog
 - users-groups
 - ssh

# The modules that run in the 'config' stage
cloud_config_modules:
 - keyboard
 - locale
 - set-passwords
 - grub-dpkg
 - apt-pipelining
 - apt-configure
 - ntp
 - timezone
 - disable-ec2-metadata
 - runcmd

# The modules that run in the 'final' stage
cloud_final_modules:
 - package-update-upgrade-install
 - write-files-deferred
 - scripts-vendor
 - scripts-per-once
 - scripts-per-boot
 - scripts-per-instance
 - scripts-user
 - ssh-authkey-fingerprints
# - keys-to-console
 - install-hotplug
# - phone-home
 - final-message
 - power-state-change

runcmd:
  - systemctl enable qemu-guest-agent.service

# System and/or distro specific settings
# (not accessible to handlers/transforms)
system_info:
  # This will affect which distro class gets used
  distro: debian
  # Other config here will be given to the distro class and/or path classes
  paths:
     cloud_dir: /var/lib/cloud/
     templates_dir: /etc/cloud/templates/
  package_mirrors:
     - arches: [default]
      failsafe:
        primary: https://deb.debian.org/debian
        security: https://deb.debian.org/debian-security
  ssh_svcname: ssh

Cleanup script:

#!/usr/bin/env bash
# =============================================================================
# cloud-init Cleanup & Uninstall Script
# Generated from: cloud.cfg (Debian, default user: debian)
#
# What this script does:
#   1. Reverses all cloud-init module side effects
#   2. Removes packages installed by cloud-init modules
#   3. Restores config files to pre-cloud-init state where possible
#   4. Cleans up cloud-init's own state/cache
#
# Usage:
#   sudo bash cloudinit-cleanup.sh [--dry-run] [--full-uninstall]
#
#   --dry-run         Print what would be done, make no changes
#   --full-uninstall  Also remove cloud-init itself (not just its effects)
#
# WARNING: Run this only on instances you intend to decommission or reprovision.
#          Some operations (hostname reset, user removal) are destructive.
# =============================================================================

set -euo pipefail

# ── Colour helpers ────────────────────────────────────────────────────────────
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'

info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
ok()      { echo -e "${GREEN}[OK]${RESET}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
danger()  { echo -e "${RED}[DANGER]${RESET} $*"; }
section() { echo -e "\n${BOLD}━━━  $* ━━━${RESET}"; }

# ── Argument parsing ──────────────────────────────────────────────────────────
DRY_RUN=false
FULL_UNINSTALL=false

for arg in "$@"; do
 case "$arg" in
    --dry-run)         DRY_RUN=true ;;
    --full-uninstall)  FULL_UNINSTALL=true ;;
    --help|-h)
      sed -n '3,20p' "$0" | sed 's/^# \?//'
     exit 0
      ;;
    *)
     echo "Unknown argument: $arg" >&2
     exit 1
      ;;
 esac
done

# ── Root check ────────────────────────────────────────────────────────────────
if [[ $EUID -ne 0 ]]; then
  danger "This script must be run as root (sudo)."
 exit 1
fi

# ── Dry-run wrapper ───────────────────────────────────────────────────────────
# All destructive calls go through run() so --dry-run is respected everywhere
run() {
 if $DRY_RUN; then
   echo -e "  ${YELLOW}[DRY-RUN]${RESET} $*"
 else
   eval "$@"
 fi
}

$DRY_RUN && warn "DRY-RUN mode — no changes will be made.\n"

# =============================================================================
# 1. CLOUD_INIT_MODULES — init stage
# =============================================================================

# ── ca-certs ──────────────────────────────────────────────────────────────────
section "ca-certs"
info "Removing custom CAs added by cloud-init..."

# cloud-init drops certs into /usr/local/share/ca-certificates/
if ls /usr/local/share/ca-certificates/cloud-init-* &>/dev/null 2>&1; then
  run "rm -f /usr/local/share/ca-certificates/cloud-init-*"
  run "update-ca-certificates --fresh"
  ok "Custom CAs removed and trust store rebuilt."
else
  info "No cloud-init-managed custom CAs found."
fi

run "rm -f /var/lib/cloud/instance/sem/config_ca_certs"

# ── rsyslog ───────────────────────────────────────────────────────────────────
section "rsyslog"
info "Removing cloud-init rsyslog configuration..."
run "rm -f /etc/rsyslog.d/20-cloud-init.conf"
if systemctl is-active --quiet rsyslog 2>/dev/null; then
  run "systemctl restart rsyslog"
  ok "rsyslog restarted."
fi
run "rm -f /var/lib/cloud/instance/sem/config_rsyslog"

# ── users-groups (default user: debian) ──────────────────────────────────────
section "users-groups — default user 'debian'"
warn "Removing user 'debian' and their home directory."
warn "Ensure you have another admin account before doing this!"

if id debian &>/dev/null 2>&1; then
 # Kill any active sessions for this user first
 run "pkill -u debian || true"
  run "deluser --remove-home debian 2>/dev/null || userdel -r debian 2>/dev/null || true"
  ok "User 'debian' removed."
else
  info "User 'debian' does not exist, skipping."
fi

# Remove groups that were created for this user (only if empty/unused)
for grp in adm audio cdrom dialout dip floppy plugdev video; do
 if getent group "$grp" &>/dev/null; then
    info "Group '$grp' exists (system group — leaving in place)."
 fi
done

run "rm -f /var/lib/cloud/instance/sem/config_users_groups"

# =============================================================================
# 2. CLOUD_CONFIG_MODULES — config stage
# =============================================================================

# ── snap ──────────────────────────────────────────────────────────────────────
section "snap"
if command -v snap &>/dev/null; then
  info "Removing all installed snaps..."
 # Snaps must be removed in dependency order; loop until none left
 while snap list 2>/dev/null | grep -v "^Name" | grep -q .; do
    snap list 2>/dev/null | awk 'NR>1 {print $1}' | while read -r pkg; do
      run "snap remove --purge '$pkg' 2>/dev/null || true"
   done
 done

  info "Stopping and disabling snapd..."
  run "systemctl stop snapd.service snapd.socket snapd.seeded.service 2>/dev/null || true"
  run "systemctl disable snapd.service snapd.socket 2>/dev/null || true"

  info "Removing snapd package..."
  run "apt-get purge -y snapd 2>/dev/null || true"

  info "Removing snap directories..."
  run "rm -rf /snap /var/snap /var/lib/snapd /var/cache/snapd /root/snap"
  run "rm -rf /home/*/snap"

  info "Removing snap loopback mounts..."
  mount | grep '/snap/' | awk '{print $3}' | while read -r mp; do
    run "umount '$mp' 2>/dev/null || true"
 done

  ok "snap and snapd fully removed."
else
  info "snap not installed, skipping."
fi

run "rm -f /var/lib/cloud/instance/sem/config_snap"

# ── ssh-import-id ─────────────────────────────────────────────────────────────
section "ssh-import-id"
info "Removing ssh-import-id if installed..."
run "apt-get purge -y ssh-import-id 2>/dev/null || true"
# Imported keys would have been written to the user's authorized_keys
if [[ -f /home/debian/.ssh/authorized_keys ]]; then
  warn "Review /home/debian/.ssh/authorized_keys for externally imported keys."
fi
run "rm -f /var/lib/cloud/instance/sem/config_ssh_import_id"

# ── set-passwords ─────────────────────────────────────────────────────────────
section "set-passwords"
info "Locking 'debian' user password (enforcing key-only auth)..."
run "passwd -l debian 2>/dev/null || true"

info "Ensuring SSH password authentication is disabled..."
if [[ -f /etc/ssh/sshd_config ]]; then
  run "sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config"
  run "grep -q 'PasswordAuthentication no' /etc/ssh/sshd_config || echo 'PasswordAuthentication no' >> /etc/ssh/sshd_config"
  run "systemctl reload ssh 2>/dev/null || true"
fi
run "rm -f /var/lib/cloud/instance/sem/config_set_passwords"
ok "Password auth locked down."

# ── grub-dpkg ─────────────────────────────────────────────────────────────────
section "grub-dpkg"
info "Clearing grub-dpkg debconf seeding..."
run "echo '' | debconf-set-selections <<< 'grub-pc grub-pc/install_devices multiselect' 2>/dev/null || true"
run "rm -f /var/lib/cloud/instance/sem/config_grub_dpkg"
ok "grub-dpkg debconf state cleared (GRUB install device reset to empty)."

# ── disable-ec2-metadata ──────────────────────────────────────────────────────
section "disable-ec2-metadata"
info "Re-enabling EC2 metadata service access (removing block if present)..."
run "rm -f /run/cloud-init/enabled"
run "iptables -D OUTPUT -d 169.254.169.254 -j DROP 2>/dev/null || true"
run "ip6tables -D OUTPUT -d fd00:ec2::254 -j DROP 2>/dev/null || true"
run "rm -f /var/lib/cloud/instance/sem/config_disable_ec2_metadata"

# ── byobu ─────────────────────────────────────────────────────────────────────
section "byobu"
info "Removing byobu configuration and package..."
run "rm -rf /etc/byobu"
run "rm -f /etc/profile.d/Z97-byobu.sh /etc/profile.d/byobu.sh"
run "rm -rf /home/debian/.byobu /root/.byobu"
run "apt-get purge -y byobu 2>/dev/null || true"
run "rm -f /var/lib/cloud/instance/sem/config_byobu"
ok "byobu removed."

# =============================================================================
# 3. CLOUD_FINAL_MODULES — final stage
# =============================================================================

# ── package-update-upgrade-install ───────────────────────────────────────────
section "package-update-upgrade-install"
warn "Packages installed via cloud-init's package module cannot be auto-detected."
warn "Review your user-data 'packages:' list and remove manually if needed."
run "rm -f /var/lib/cloud/instance/sem/config_package_update_upgrade_install"

# ── fan ───────────────────────────────────────────────────────────────────────
section "fan (Ubuntu Fan networking)"
if command -v fanctl &>/dev/null || dpkg -s ubuntu-fan &>/dev/null 2>&1; then
  info "Removing ubuntu-fan networking..."
  run "systemctl stop fanatic 2>/dev/null || true"
  run "apt-get purge -y ubuntu-fan 2>/dev/null || true"
  run "rm -f /etc/network/fan"
  ok "ubuntu-fan removed."
else
  info "ubuntu-fan not installed, skipping."
fi
run "rm -f /var/lib/cloud/instance/sem/config_fan"

# ── landscape ────────────────────────────────────────────────────────────────
section "landscape"
if dpkg -s landscape-client &>/dev/null 2>&1; then
  info "Removing Landscape client..."
  run "systemctl stop landscape-client 2>/dev/null || true"
  run "apt-get purge -y landscape-client landscape-common 2>/dev/null || true"
  run "rm -rf /etc/landscape /var/lib/landscape"
  ok "Landscape client removed."
else
  info "Landscape client not installed, skipping."
fi
run "rm -f /var/lib/cloud/instance/sem/config_landscape"

# ── lxd ──────────────────────────────────────────────────────────────────────
section "lxd"
if command -v lxd &>/dev/null; then
  info "Removing LXD and all containers/images..."
  run "lxc list --format csv -c n 2>/dev/null | while read -r c; do lxc delete --force \"\$c\"; done || true"
  run "snap remove --purge lxd 2>/dev/null || apt-get purge -y lxd lxd-client liblxc1 2>/dev/null || true"
  run "rm -rf /var/lib/lxd /var/snap/lxd"
  ok "LXD removed."
else
  info "LXD not installed, skipping."
fi
run "rm -f /var/lib/cloud/instance/sem/config_lxd"

# ── puppet ───────────────────────────────────────────────────────────────────
section "puppet"
if command -v puppet &>/dev/null || dpkg -s puppet-agent &>/dev/null 2>&1; then
  info "Removing Puppet agent..."
  run "systemctl stop puppet 2>/dev/null || true"
  run "apt-get purge -y puppet puppet-agent 2>/dev/null || true"
  run "rm -rf /etc/puppet /var/lib/puppet /opt/puppetlabs"
  ok "Puppet removed."
else
  info "Puppet not installed, skipping."
fi
run "rm -f /var/lib/cloud/instance/sem/config_puppet"

# ── chef ─────────────────────────────────────────────────────────────────────
section "chef"
if command -v chef-client &>/dev/null; then
  info "Removing Chef client..."
  run "systemctl stop chef-client 2>/dev/null || true"
  run "apt-get purge -y chef 2>/dev/null || true"
  run "rm -rf /etc/chef /var/log/chef /var/lib/chef /var/cache/chef /var/backups/chef /var/run/chef"
  ok "Chef removed."
else
  info "Chef not installed, skipping."
fi
run "rm -f /var/lib/cloud/instance/sem/config_chef"

# ── mcollective ───────────────────────────────────────────────────────────────
section "mcollective"
if dpkg -s mcollective &>/dev/null 2>&1; then
  info "Removing MCollective..."
  run "systemctl stop mcollective 2>/dev/null || true"
  run "apt-get purge -y mcollective 2>/dev/null || true"
  run "rm -rf /etc/mcollective"
  ok "MCollective removed."
else
  info "MCollective not installed, skipping."
fi
run "rm -f /var/lib/cloud/instance/sem/config_mcollective"

# ── salt-minion ───────────────────────────────────────────────────────────────
section "salt-minion"
if command -v salt-minion &>/dev/null || dpkg -s salt-minion &>/dev/null 2>&1; then
  info "Removing Salt minion..."
  run "systemctl stop salt-minion 2>/dev/null || true"
  run "apt-get purge -y salt-minion salt-common 2>/dev/null || true"
  run "rm -rf /etc/salt /var/cache/salt /var/log/salt /var/run/salt"
  ok "Salt minion removed."
else
  info "salt-minion not installed, skipping."
fi
run "rm -f /var/lib/cloud/instance/sem/config_salt_minion"

# ── reset_rmc ─────────────────────────────────────────────────────────────────
section "reset_rmc (IBM Power)"
# Only relevant on IBM Power hardware; safe no-op on other platforms
run "rm -f /var/lib/cloud/instance/sem/config_reset_rmc"
info "reset_rmc semaphore cleared (no-op on non-IBM-Power hardware)."

# ── phone-home ────────────────────────────────────────────────────────────────
section "phone-home"
info "Clearing phone-home state..."
run "rm -f /var/lib/cloud/instance/sem/config_phone_home"
# Revoke any iptables allow-rule if phone-home was whitelisted
info "If you added firewall rules for phone-home, remove them manually."
ok "phone-home state cleared."

# ── keys-to-console / ssh-authkey-fingerprints ────────────────────────────────
section "keys-to-console / ssh-authkey-fingerprints"
info "Removing console key output scripts..."
run "rm -f /etc/profile.d/ssh-key-fingerprints.sh"
run "rm -f /var/lib/cloud/instance/sem/config_keys_to_console"
run "rm -f /var/lib/cloud/instance/sem/config_ssh_authkey_fingerprints"

# =============================================================================
# resolv.conf — restore after systemd-resolved removal
# =============================================================================
section "resolv.conf reset (systemd-resolved removed)"

RESOLV="/etc/resolv.conf"

# Detect what's currently there
if [[ -L "$RESOLV" ]]; then
  info "Found symlink: $RESOLV -> $(readlink -f "$RESOLV")"
  info "Removing dangling symlink left by systemd-resolved..."
  run "rm -f '$RESOLV'"
else
  info "$RESOLV is a plain file — backing it up before overwriting..."
  run "cp '$RESOLV' '${RESOLV}.bak.$(date +%Y%m%d%H%M%S)'"
fi

# Write a static resolv.conf with sensible production defaults.
# Adjust nameservers to match your infrastructure (internal DNS, VPC resolver, etc.)
info "Writing static $RESOLV..."
run "cat > '$RESOLV' <<'EOF'
# /etc/resolv.conf — managed manually (systemd-resolved is not installed)
nameserver 1.1.1.1
nameserver 8.8.8.8
nameserver 2606:4700:4700::1111

# search your.internal.domain

options timeout:2 attempts:3 rotate
EOF"


run "chmod 644 '$RESOLV'"
run "chown root:root '$RESOLV'"

# Verify DNS resolution works with the new config
info "Testing DNS resolution..."
if $DRY_RUN; then
 echo -e "  ${YELLOW}[DRY-RUN]${RESET} getent hosts debian.org"
elif getent hosts debian.org &>/dev/null; then
  ok "DNS resolution working."
else
  warn "DNS resolution test failed — check nameservers in $RESOLV"
fi

# =============================================================================
# SUMMARY
# =============================================================================
section "Cleanup Complete"

echo ""
echo -e "${BOLD}Actions performed:${RESET}"
echo "  ✓ Hostname reset to 'localhost' (/etc/hostname, /etc/hosts)"
echo "  ✓ Custom CA certificates removed, trust store rebuilt"
echo "  ✓ rsyslog cloud-init config removed"
echo "  ✓ User 'debian' removed (with home directory)"
echo "  ✓ SSH host keys regenerated"
echo "  ✓ snap/snapd removed"
echo "  ✓ ssh-import-id removed"
echo "  ✓ Password authentication locked (key-only enforced)"
echo "  ✓ grub-dpkg debconf seeding cleared"
echo "  ✓ byobu removed"
echo "  ✓ hotplug udev rule removed"
echo "  ✓ phone-home state cleared"
echo "  ✓ All cloud-init semaphores and cache cleared"
echo "  ✓ Landscape / LXD / Puppet / Chef / Salt / MCollective checked and removed if present"
echo ""
echo -e "${YELLOW}Manual review required:${RESET}"
echo "  ⚠  Files written by write-files — check your user-data for target paths"
echo "  ⚠  Packages installed via 'packages:' in user-data"
echo "  ⚠  Scripts run from scripts-user / scripts-per-instance / scripts-vendor"
echo "  ⚠  /home/debian/.ssh/authorized_keys — if user was not removed"
echo "  ⚠  Any firewall rules added for phone-home or disable-ec2-metadata"
echo ""
if $FULL_UNINSTALL; then
 echo -e "${RED}cloud-init has been fully removed from this system.${RESET}"
else
 echo -e "${CYAN}cloud-init is still installed. Run with --full-uninstall to remove it too.${RESET}"
fi
echo ""
echo -e "${GREEN}Done.${RESET}"