Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Stefan Bethke 0b8c5bb757
All checks were successful
CI / test (push) Successful in 2m14s
Docs / build-and-deploy (push) Successful in 40s
Release / build-binaries (amd64, linux) (push) Successful in 2m35s
CI / cross-build (amd64, linux) (push) Successful in 1m38s
Release / release (push) Successful in 43s
Rebuild the staged local mirror each build, so no stale kernel lingers
kernel-install failed with `Failed to open kernel image file
'/boot/vmlinuz-6.12.107+deb13-amd64\n6.12.94+deb13-amd64'`: two
linux-image packages had landed in the chroot, so `ls /lib/modules`
returned two lines and KVER got spliced newline-and-all into the path.

The two kernels came from the local mirror under cache:. pool/ kept the
previous build's packages, regenerateIndex indexes pool/ wholesale, and
so the Packages handed to debootstrap offered both the June kernel
(linux-image-6.12.94+deb13-amd64, plus linux-image-amd64 =6.12.94-1) and
the August one this build had just downloaded. debootstrap installed
both. Note the pool doesn't accumulate without bound -- the live cache
had 186 files and no duplicate package names at all -- one build's
leftovers beside the next build's downloads is enough.

resetStagedMirror clears pool/, dists/ and .apt-move/ before staging, so
the generated mirror holds exactly what this build downloaded. That is
free: archivesDir is already wiped each run, so apt-get download
re-fetches every package from mirror regardless of pool/'s contents, and
nothing reads pool/ except debootstrap at the end of the same build.
Despite the name, cache: never saved a package download -- pool/ is a
per-build staging area shaped like an archive, and what genuinely
persists across builds is apt's index state under .build. The docs and
spec.go comments claimed otherwise; corrected rather than left standing
next to the new behavior.

apt-move's own DELETE=yes is set too (with MAXDELETE=100, whose
delete-percentage guard would otherwise only abort builds over a pool
that's rebuilt anyway), but it is deliberately not what fixes this, and
could not be: apt-move treats a package as obsolete only once it is
unreachable from its own .apt-move index, which happens when a newer
version of the same *name* displaces it. Each kernel ABI is its own
package name, so the stale linux-image-6.12.94+deb13-amd64 survives it
untouched. Verified against a copy of a real cache -- with DELETE=yes
alone the regenerated index still listed both kernels; with the reset it
lists one.

As defense in depth, the configure-system-serial preset now counts
/lib/modules before reading it and fails loudly naming what it found,
rather than building a nonsense path. Two kernels in a template image is
a bug worth surfacing, not something to paper over by picking one. The
count also catches zero, which the old code would have turned into
/boot/vmlinuz- just as silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 18:26:46 +00:00
.forgejo/workflows Switch docs theme to mkdocs-material 2026-09-04 21:53:21 +00:00
docs Rebuild the staged local mirror each build, so no stale kernel lingers 2026-09-15 18:26:46 +00:00
example-specs Unify file/contents/url naming across copy, run, include, and gpg-key 2026-09-06 11:02:39 +00:00
internal Rebuild the staged local mirror each build, so no stale kernel lingers 2026-09-15 18:26:46 +00:00
.gitignore Initial version 2026-09-04 08:06:17 +00:00
go.mod Initial version 2026-09-04 08:06:17 +00:00
go.sum Initial version 2026-09-04 08:06:17 +00:00
LICENSE Initial version 2026-09-04 08:06:17 +00:00
main.go Build directly on qcow2 via qemu-nbd, skipping the raw->qcow2 conversion 2026-09-06 17:01:26 +00:00
mkdocs.yml Switch docs theme to mkdocs-material 2026-09-04 21:53:21 +00:00
nfpm.yaml Build and publish a .deb in the release workflow 2026-09-04 08:34:56 +00:00
README.md Split rationale into a new design.md, trim the two main docs 2026-09-04 11:11:39 +00:00

PVE Template Builder

PVE Template Builder creates VM templates in a Proxmox Virtual Environment cluster. It uses a specification file that specifies the way the disk image is built, including packages to install, configuration adjustments, and shell-based customizations, and it specifies the VM parameters. This project has been developed with the help of generative AI.

Documentation

  • docs/building-template.md — the full build sequence, host requirements, command-line flags, and how each step (including debootstrap and the optional local mirror) is actually executed.
  • docs/template-spec.md — reference for every option in the specification file.
  • docs/design.md — the reasoning behind non-obvious choices in the two documents above.

Installation

Download a release for Linux/amd64 — currently the only platform built — from Releases:

  • .deb: apt install ./pve-template-builder-<version>-amd64.deb. This also pulls in every tool pve-template-builder needs at build time (see Requirements) automatically, via the package's own apt dependencies.
  • .tar.gz: a plain binary — extract it onto $PATH yourself, then install those tools by hand (see Quick start below).

Or build from source (Go 1.26+): go build ., same manual tool installation as the .tar.gz path above.

Quick start

apt install debootstrap gdisk dosfstools e2fsprogs qemu-utils apt-move apt-utils
sudo pve-template-builder spec.yaml

pve-template-builder must run as root (loop devices, mounts and chroot all require it). See docs/building-template.md for the full requirements list and what each tool is used for.