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_keys — user 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/tmpfirst; - one that isn't a URL but still contains a
/is instead a path to a local.debon 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: onstorage, attached as a SCSI disk on a virtio-scsi controller, imported from the volume uploaded toimport-storage, plus whateverscsi-diskspecifies.efidisk0: a UEFI variable-store disk onstorage(distinct from the image's own EFI System Partition, which holds the actual bootloader files), with options fromefi-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.