Skip to content

Specification File Reference

The specification file given to pve-template-builder is YAML, with three top-level sections: disk, builder and template. Relative host paths inside the spec — a file field, or include's plain-string form (see File sources) — are resolved relative to the spec file's own directory.

For an explanation of the overall build sequence and how each of these options is actually used, see How a Template Is Built. For the reasoning behind non-obvious choices, see Design Notes.

disk

Size and partitioning of the target disk image. See Create the raw disk image and Partition it.

Field Description
format Partitioning scheme. Only gpt-uefi is supported (and is the default if omitted): a GPT table with a fixed 512 MiB EFI System Partition plus a second partition using the rest of the disk.
size Required. Size of the disk image, e.g. 20G. Suffixes K/M/G/T are binary (1024-based); a bare number is bytes.
filesystem Filesystem for the main OS partition: ext4 (default), xfs, or btrfs.

builder

Everything that constructs the OS filesystem: bootstrap and steps.

builder.bootstrap

Selects and configures the bootstrap method. Currently only debootstrap is supported, via builder.bootstrap.debootstrap.

builder.bootstrap.debootstrap

Parameters for debootstrap. See Bootstrap the OS and Bootstrapping with a local package mirror.

Field Description
preset Selects a named bundle of defaults for suite/mirror/arch/include, plus an automatic post-bootstrap boot-setup step. debian-serial and ubuntu-serial are supported. Empty (the default) changes nothing — every field below behaves exactly as documented. See Presets.
suite The release/suite name to install, e.g. stable, trixie. Required unless preset supplies a default.
mirror URL of the mirror to install from, e.g. https://deb.debian.org/debian. Also becomes the base apt source written into the built system. Required unless preset supplies a default.
cache A directory holding a local package mirror that debootstrap installs from instead of mirror, so its packages only need to be downloaded once across builds. Requires apt-move and apt-utils (apt-ftparchive) on the build host — see Bootstrapping with a local package mirror.
arch Target architecture, e.g. amd64. Defaults to debootstrap's own default (the host's architecture) if unset (and no preset sets one) — except when cache is set, where the host's dpkg --print-architecture is resolved explicitly and used consistently throughout.
components Space-separated archive components, e.g. main contrib. Defaults to main wherever a default is needed (the base apt source, and the local cache's index) if unset.
exclude A list of package names to exclude from the base install, joined with , to build debootstrap --exclude.
include A list of extra package names to add to the base install, joined with , to build debootstrap --include. ca-certificates is added automatically when mirror is https:// and it isn't already listed — see ca-certificates and HTTPS mirrors. If preset is set, this list is added to the preset's own packages rather than replacing them.
resolve-deps Whether debootstrap should resolve dependencies of the packages it installs. true (the default) if unset; false passes --no-resolve-deps.
variant debootstrap variant, e.g. minbase, buildd (debootstrap --variant).
user Creates a login user once the base system is bootstrapped. Omit entirely for no user. See builder.bootstrap.debootstrap.user.

Presets (builder.bootstrap.debootstrap.preset)

A preset bundles up the debootstrap config and builder.steps boilerplate that every bootable Debian image needs, so a spec only has to state what makes this image different. suite/mirror/arch/components set explicitly in the spec still take priority over the preset's own defaults; include/exclude are added to, not replaced by, the preset's own lists. Applied during validation (so pve-template-builder --dry-run shows its effect), before the required-field checks above — a spec using a preset can leave suite and mirror out entirely.

Two presets are supported: debian-serial and ubuntu-serial.

debian-serial

Field Value
suite stable
mirror https://deb.debian.org/debian
arch amd64
components (unset — debootstrap's own main default)
include linux-image-amd64, qemu-guest-agent, systemd-boot, systemd-boot-efi, systemd-boot-efi-amd64-signed
exclude (none)

ubuntu-serial

Field Value
suite noble (Ubuntu 24.04 LTS)
mirror https://archive.ubuntu.com/ubuntu
arch amd64
components main universe (qemu-guest-agent lives in universe)
include linux-image-generic, initramfs-tools, fdisk, qemu-guest-agent, systemd-boot, systemd-boot-efi
exclude usrmerge

Compared to debian-serial, ubuntu-serial skips the signed systemd-boot-efi-amd64-signed package (Ubuntu dropped it from its archive — this is fine; why), includes fdisk/initramfs-tools that debian-serial doesn't need, and excludes usrmerge — see Why ubuntu-serial includes fdisk, initramfs-tools, and excludes usrmerge for why each of those is needed. suite is deliberately noble (24.04 LTS), not the newest release — see Why ubuntu-serial targets noble, not the newest release for the (verified) reason.

builder.bootstrap.debootstrap.user

Creates a login user, once the base system is bootstrapped and before any of builder.steps runs (and, if preset is also set, before its own boot-setup step — see Presets above). Omit the whole user section for no user at all.

Field Description
username The account to create. Required whenever user is set at all.
password The account's clear-text initial password, set via chpasswd. Optional — a cloud-init drive (ide2: <storage>:cloudinit plus parameters.ciuser/cipassword, or an SSH-key-only login) can set the password instead, without it ever appearing in the spec file itself.
authorized_keys Populates ~<username>/.ssh/authorized_keys, creating .ssh itself (mode 700, owned by the new user) alongside it. Optional. The same file/contents/url choice as elsewhere (see File sources), at most one; a bare scalar value is shorthand for contents.
user:
  username: ubuntu
  password: ubuntu
  authorized_keys: |
    ssh-ed25519 AAAA... me@laptop

# or, reading the key(s) from a file:
user:
  username: ubuntu
  authorized_keys:
    file: ../custom/authorized_keys

# or, downloading them:
user:
  username: ubuntu
  authorized_keys:
    url: https://github.com/example.keys

Internally, this is equivalent to writing the shell commands the "Configure a user" step in debian.yaml/ubuntu.yaml uses by hand (useradd -m -G root,sudo -s /bin/bash <username>, plus chpasswd and the .ssh setup) as a run step, followed by a copy step for authorized_keysuser just saves writing that out (and shell-quotes username/password for you). useradd's -G root,sudo needs a sudo group to exist, which means the sudo package must already be installed by then — neither preset's own package set includes it (see Presets above), so list it in debootstrap.include yourself, same as ubuntu.yaml does.

Shared boot setup

Both presets prepend the same step (named "Configure system", so it's still identifiable in logs) to builder.steps, ahead of any the spec defines, that: sets the machine id (systemd-machine-id-setup); writes /etc/kernel/cmdline and /etc/fstab from the root and EFI partitions' UUIDs (using disk.filesystem, or its ext4 default, as the root filesystem type — the serial console is enabled with console=tty0 console=ttyS0,115200, hence "-serial"); installs systemd-boot (bootctl install) with a 1-second boot menu timeout; registers the installed kernel with it (kernel-install add); and installs a grow-root.service that, once on first boot, grows the root partition into whatever free space the disk was given beyond the image's own size and resizes its filesystem to match. Because this runs as an ordinary run step, it needs the same chroot environment any other step does — see Run the build steps. Its script lives in this package's own source tree (internal/spec/presetfiles/common/configure-system-serial.yaml — shared, since nothing in it is actually distribution-specific), compiled into the binary via go:embed rather than kept as a Go string literal.

builder.steps

A list of steps executed in order to complete package installation and configuration. Each entry is a mapping with exactly one step-kind key, selecting the step type: copy, apt-source, package, run, or include. See Run the build steps for how each is executed.

example-specs/example.yaml shows every step kind (and every run/copy variant) in one file. It's a syntax reference, not a working spec — building it as-is will fail, since some of it (a run step's file script, the template.pve connection details) is deliberately fake. See debian.yaml/ubuntu.yaml in the same directory for specs that actually build a working template.

An optional name key alongside the step-kind key identifies the step in build logging and in the error message if it fails, e.g.:

- name: install base packages
  package: [curl, vim]

If unset, a default is derived from the step itself: the apt-source's own name for apt-source, the package list for package, the command (just its first line, if it spans several) or the file/url executable's filename for run, and the destination path (plus source, for a file copy) for copy.

File sources: file, contents, url

Several places below share the same three-way choice for where a piece of content comes from: a copy step, apt-source's gpg-key, a run step, an include step, and debootstrap.user.authorized_keys.

Field Description
file A path on the host, resolved relative to the spec file's own directory unless already absolute.
contents The content, written out literally.
url A URL to download the content from.

Each place below says whether these are required (and, if so, whether exactly one or at most one) and whether it also accepts a bare scalar value as shorthand for one of the three — which one differs by context: run and authorized_keys default a bare string to contents (it's literal content, same as writing contents: <value>); include defaults it to file (it's a path, same as writing file: <value>). copy and gpg-key have no such shorthand — they're always mappings.

(The package step's own item syntax is unrelated to all of this — see Step package — since a package name and a file/url triggering a download/copy share the same list rather than being distinguished by key.)

Step copy: copy files into target

Copies a file or directory from the host filesystem into the target, downloads a file from a URL, or writes a literal string out as a file. Exactly one of file/contents/url is required (see File sources).

- copy:
    contents: "example\n"
    to: /etc/hostname
    mode: "644"
- copy:
    file: ../custom/authorized_keys
    to: /root/.ssh/authorized_keys
    owner: root
    group: root
    mode: "600"
- copy:
    url: https://example.com/some-tool.tar.gz
    to: /opt/some-tool
    unpack: true
Field Description
file Source path on the host, resolved relative to the spec file's directory (unless already absolute). Copied recursively if it's a directory. Mutually exclusive with contents/url.
contents Literal content to write to to as a single file. Mutually exclusive with file/url. Cannot be combined with unpack.
url URL to download and write to to as a single file (or, with unpack: true, to extract). Mutually exclusive with file/contents.
unpack Boolean, default false. Treats the file/url source as an archive and extracts its contents into to instead of copying/writing the archive file itself. The format (tar+gzip, tar+bzip2, or zip) is detected from the archive's content, not its file extension.
to Required. Destination path inside the target: the file/directory name for file/url/contents, or the directory extracted into for unpack.
owner Owner (name or numeric id) to set, recursively, on every file/directory written by the step. Names are resolved against the target's own /etc/passwd.
group Group (name or numeric id) to set, recursively, on every file/directory written by the step. Names are resolved against the target's own /etc/group.
mode Octal permission mode, applied recursively to every file/directory written by the step (for contents, just the one file it writes). If unset, source permissions are kept as-is (or, for contents with no mode, 0644 is used).

For file/url (including unpack), mode's r/w bits are applied literally to every copied/extracted entry, but its x bit is applied "smartly", per owner/group/other category: that category only gets x if mode also grants it r and the corresponding source entry (the file on the host, or the file's entry in the archive) already had some execute bit set. This lets one mode standardize permissions across a whole tree while still preserving the distinction between scripts/binaries and plain data files. For contents, which has no source entry to take an x bit from, mode is applied literally instead.

Step apt-source: APT source definition

Defines an additional APT source, written in deb822 (.sources) format.

- apt-source:
    name: docker
    gpg-key:
      url: https://download.docker.com/linux/debian/gpg
      filename: docker.asc
    source: |
      Types: deb
      URIs: https://download.docker.com/linux/debian
      Suites: trixie
      Components: stable
      Architectures: amd64
      Signed-By: /etc/apt/keyrings/docker.asc
Field Description
name Required. Used as the filename: /etc/apt/sources.list.d/<name>.sources.
sources (or source) Required. The deb822-format source content, written as-is.
gpg-key.file/gpg-key.contents/gpg-key.url Where to get the repository's GPG signing key from (see File sources) — at most one, and gpg-key may be omitted entirely if the source needs no signing key.
gpg-key.filename Filename to store the key as, under /etc/apt/keyrings/. Required if gpg-key is set at all.

Step package: install packages

A list of items to install (apt-get update && apt-get install -y <items...>). Each one is normally an apt package name, passed straight through, but:

  • one that looks like a URL (has a scheme, e.g. https://...) is downloaded straight into the chroot's /tmp first;
  • one that isn't a URL but still contains a / is instead a path to a local .deb on the host — resolved relative to the spec file's directory, unless already absolute — and copied into the chroot's /tmp.

(This is its own separate syntax, not the file/contents/url keys described in File sources — a package item is a single string, sniffed for a URL scheme or a / rather than a mapping.)

Either way, the item is then installed by that /tmp path instead of by name, in the same apt-get install invocation as every plain package name in the same step — so apt resolves dependencies (a downloaded .deb's included) against the whole set at once, not in isolation. The staged copy is removed again once apt-get install returns.

- package:
    - curl
    - vim
    - htop
    - https://example.com/some-tool_1.0_amd64.deb
    - ../custom/another-tool_2.0_amd64.deb

Step run: run a shell command or host executable

- run: echo hi
- run:
    contents: echo hi
- run:
    file: scripts/setup.sh
- run:
    url: https://example.com/setup.sh

Either a plain command string (run: <command>, shorthand for contents) or a mapping with exactly one of file/contents/url (see File sources):

Field Description
contents A shell command string, run as-is inside the chroot — equivalent to the plain-string form.
file Path to an executable on the host, resolved relative to the spec file's directory (unless already absolute), same as a copy step's file. Copied into the chroot under /tmp, executed there directly (not through a shell), and removed again afterwards.
url An executable to download instead, run exactly like file (downloaded to a temporary file first).

Step include: include steps from another file

- include: extras/docker.yaml
- include:
    file: extras/docker.yaml
- include:
    contents: |
      steps:
        - run: echo hi
- include:
    url: https://example.com/extras.yaml

A YAML file (or, with contents, inline YAML text) with a single top-level steps key, holding a list in exactly the same format as builder.steps itself — in place of the include step, every step in that list runs as if it had been written directly into builder.steps there. Exactly one of file/contents/url (see File sources); the plain-string form (include: <path>) is shorthand for file. file is resolved relative to the spec file's own directory, unless already absolute (the same convention a copy step's file uses). url is the one place a specification file's own parsing (and so --dry-run too) makes a network request — every other URL in the spec is instead only fetched later, during the actual build.

An included file may itself include further files, resolved the same way, to any depth. Including the same source twice — directly, or by way of a cycle (a.yaml includes b.yaml includes a.yaml) — is not an error: the repeat is logged as a warning and simply skipped, rather than duplicating (or, for a cycle, infinitely re-expanding) that file's steps. file/url/contents are tracked separately for this, so (for example) a file and a url that happen to serve identical content are never considered duplicates of each other.

debian-serial/ubuntu-serial's own "Configure system" step (see Presets above) is itself just an include step under the hood, resolved against this package's compiled-in presetfiles/ instead of the spec file's directory — the same mechanism, just with a different filesystem behind it.

template

Parameters needed to create the template VM in Proxmox VE, via template.pve.

template.pve

See Publish to Proxmox VE for the full upload/create sequence.

Field Description
api-endpoint Base URL of the PVE API, e.g. https://pve.example.com:8006. If the PVE_URL environment variable is set, it's used instead (and takes precedence). One of the two is required.
api-token The token sent as Authorization: PVEAPIToken=<value>. Must be the full token, in the form user@realm!tokenid=secret (e.g. root@pam!pve-template-builder=00000000-0000-0000-0000-000000000000) — not just the secret half shown at the end. If the PVE_API_TOKEN environment variable is set, it's used instead (and takes precedence). One of the two is required. The token's format (and, before the image upload, that it actually authenticates) is checked upfront — see Publish to Proxmox VE. It also needs the right PVE permissions/role, which don't come for free — see PVE API token setup.
vmid Required. The VM ID to create the template as.
node Required. The PVE node to create the template on.
name Required. Name of the VM. Also used to derive other object names, e.g. the built image's filename.
replace If true (the default), an existing VM with vmid is deleted (with purge and destroy-unreferenced-disks) before creating the new one. If false and the VM already exists, the build fails.
import-storage Required. Name of a PVE storage with the import content type that the built disk image is uploaded to. Only used as a staging area — see storage below for where the VM's actual disk ends up.
storage Required. Name of a PVE storage with the images content type that the VM's disks (scsi0, efidisk0) are created on. Deliberately separate from import-storage: a storage supporting import doesn't necessarily support images, or vice versa — see Publish to Proxmox VE. Can be the same storage as import-storage if it has both content types enabled.
scsi-disk A map of extra options (e.g. ssd, discard, cache, backup) merged into the scsi0 disk string, alongside the automatically-set import-from. Any option qm's disk syntax accepts for a SCSI disk can go here.
efi-disk A map of options for the efidisk0 disk, overriding the defaults efitype: 4m and pre-enrolled-keys: false. Only set pre-enrolled-keys: true if the image's bootloader is actually signed for Secure Boot — see Publish to Proxmox VE.
parameters A map of any VM parameters accepted by POST /nodes/{node}/qemu, merged directly into the create request.

Two disks are always added automatically on top of parameters, and don't need to (and can't usefully) be set there:

  • scsi0: on storage, attached as a SCSI disk on a virtio-scsi controller, imported from the volume uploaded to import-storage, plus whatever scsi-disk specifies.
  • efidisk0: a UEFI variable-store disk on storage (distinct from the image's own EFI System Partition, which holds the actual bootloader files), with options from efi-disk.

description (PVE's Notes panel) also gets a default if parameters doesn't set one of its own: Built from <spec filename> on <build date and time>, so a template can be traced back to the spec (and rough build time) that produced it.