Building a server by hand is a process that produces a unique snowflake, and the hundredth server built that way differs from the first in ways nobody can list. The template-and-seed pattern replaces the snowflake with a mold: a single prepared image holds the operating system, and every deployment stamps a fresh copy with a small per-machine configuration through cloud-init, the same first-boot engine the cloud providers run, available on every private KVM host in the fleet. The result is a deployment that takes one command and produces a machine whose whole personality is a file, reviewable, versioned and reproducible. This article builds the pattern from the template upward: preparing the image, generating the seed that carries the per-machine configuration, the user-data content in full, the cloning workflow, and the verification that turns the first boot from a hope into a check.
The pattern, one image and a small seed per machine
The architecture splits a server into two layers with a clean boundary. The template is the shared layer, the operating system, the packages, the agents and the baseline configuration, built once, updated as a versioned artifact and never booted as itself. The seed is the individual layer, a tiny file, a few kilobytes, carrying the hostname, the network configuration, the SSH keys and the machine's specific role, generated per deployment and attached to the clone at creation. Cloud-init on the template's first boot reads the seed and applies it, the machine emerges configured, and the boundary between what every machine shares and what makes this machine itself is a line drawn in configuration rather than in procedure.
The data source that makes this work on private infrastructure is NoCloud, cloud-init's mechanism for the seed, which expects a small volume carrying the files with a specific filesystem label. The seed volume holds two or three plain files, user-data with the configuration, meta-data with the instance identity, and optionally network-config for explicit network setup, and the engine finds them by the label, reads them, and runs the same first-boot stages described in the cloud-init articles earlier in this series, the same init, config and final phases, the same YAML format, the same runcmd and write_files vocabulary.
Preparing the template
The template's quality determines every deployment's ceiling, and the preparation has two routes. The build route starts from a distribution cloud image, the qcow2 artifacts every major distribution publishes for exactly this purpose, and customizes it into the site template, adding packages, agents and baseline settings, with the libguestfs tooling, virt-customize, doing the editing against the image file directly:
virt-customize -a base-bookworm.qcow2 \ --install qemu-guest-agent,htop,prometheus-node-exporter \ --run-command 'apt clean' \ --truncate /etc/machine-id
and the truncation line is the identity reset that makes every clone generate its own machine-id rather than inheriting the template's, the classic mistake that produces a fleet of machines whose monitoring all reports as one.
The second route builds the template by hand once, installs and configures the system deliberately, and then generalizes it back into a template through the sibling tool, virt-sysprep, which strips the machine-specific leftovers, SSH host keys, logs, the cloud-init state, so that the image boots as a fresh machine:
virt-sysprep -a site-template.qcow2 --enable machine-id,ssh-hostkeys,customize --operations
Cloud-init's own state deserves the explicit mention: the tool that has already run on the template must be reset, because the engine marks its work done and will not re-run without the reset, and the one-line reset, cloud-init clean, either by hand before shutdown or through virt-sysprep's operations, is the difference between a template that configures its clones and a template that boots them blank.
Generating the seed
The seed is generated per machine from the template's expectations, and the tool that assembles the volume is cloud-localds, from the cloud-image-utils package:
cloud-localds seed-app01.img user-data.yml meta-data.yml
which writes a small ISO9660 volume, labeled with the cidata label that NoCloud discovers, carrying the given files. The meta-data file is two lines:
instance-id: app01
local-hostname: app01
and the instance-id carries the identity reset discipline: a machine re-seeded with the same id will skip already-completed stages, so a redeployment, the same hostname re-cloned, gets a new instance-id, app01-20261014-1 in the site's convention, to make the boot fresh.
The user-data file is the mold's stamp, and a working per-server example covers the standard ground:
#cloud-config
hostname: app01
manage_etc_hosts: true
users:
- name: admin
groups: [sudo]
shell: /bin/bash
sudo: "ALL=(ALL) NOPASSWD:ALL"
ssh_authorized_keys:
- ssh-ed25519 AAAA... admin-key
package_update: false
runcmd:
- systemctl enable --now prometheus-node-exporter
- /usr/local/bin/register-monitoring.sh
final_message: "app01 ready after $UPTIME seconds"
The final_message line is the checkpoint in the serial console, the signal that the whole first boot completed, and the network question gets its explicit answer when the fleet needs it, the network-config file beside the seed's other files, carrying the interface addresses in cloud-init's network format so that machines with static identities arrive with their addresses already correct.
Cloning, seeding and the deployment command
The deployment assembles the pieces in one motion, and the tooling keeps it a single command on current libvirt. virt-install with the cloud-init support builds the machine from the template and the seed together:
virt-install --name app01 \ --memory 4096 --vcpus 2 \ --import \ --disk path=/var/lib/libvirt/images/app01.qcow2,backing_format=qcow2,backing_store=/var/lib/libvirt/images/site-template.qcow2 \ --disk path=/var/lib/libvirt/images/seed-app01.img \ --network network=prod,model=virtio \ --os-variant debian12 --noautoconsole
The first disk uses the qcow2 backing-store mechanism, the copy-on-write overlay that makes the clone cost seconds and megabytes, the template lying underneath unchanged, the second disk is the seed, and the --import flag skips the installation phase because the template already is the system. The backing-chain pattern composes with the storage article's snapshot discipline naturally, the clone is itself a chain that can snapshot, and the fleet's storage cost is one template plus per-machine deltas.
The libvirt-native cloud-init route exists as well, --cloud-init passing the user-data through a temporary seed the tool creates, and the choice between the explicit seed volume and the flag is a fleet preference: the explicit seed is an artifact, inspectable and regenerable, the flag is the convenience that keeps the one-command deployment flowing.
The verification loop and the fleet story
The first boot's evidence arrives on the serial console, and the console is part of the deployment command, the machine's text output visible through virsh console app01 until the final_message appears. The verification is three checks, the message itself, the SSH login with the seed's key, and the one command that proves the engine's own opinion, cloud-init status, printing done. A deployment that passes the three checks is a server, complete and identical in process to its fifty siblings, and a deployment that fails any of them has its evidence in the same console, the same logs, /var/log/cloud-init-output.log, that the earlier cloud-init articles established as the debugging ground.
The fleet story completes the pattern's economics. One template, updated monthly through the same build pipeline, every deployment one command and one seed file, every machine's personality one reviewable YAML in the infrastructure repository, and the question that ends most snowflake fleets, which servers have which configuration, answered by the repository rather than by memory. The maintenance cadence is the template's lifecycle, rebuilt or updated on schedule, the clones inheriting the new baseline as they are redeployed, and the whole discipline composing with the fleet's other machinery: the bastion access, the agent from the guest-agent article, the budgets from the cgroups article, each layer applied to a machine that arrived configured because the pattern made configuration the easy path.
## Network configuration in the seed
The machines that need static identities, the database tier, the service nodes behind their own addresses, arrive with their network already correct through the third file in the seed, network-config, written in cloud-init's network format. A static single-address setup reads:
version: 2
ethernets:
ens3:
match:
name: ens3
addresses:
- 192.168.4.21/24
routes:
- to: default
via: 192.168.4.1
nameservers:
addresses: [192.168.4.2]
and the seed carries it alongside the user-data and meta-data, passed to cloud-localds as the third argument. The match-by-name pattern survives the virtio renaming from the earlier articles, the interface that arrives as ens3 in every clone of this template, and the explicitness is the point: the machine boots once, with its address, its route and its resolver, no DHCP race, no first-login network repair, the network being one of the few parts of a server that must work before the tools that fix things are available.
The bridge and VLAN cases extend the same file with the interfaces cloud-init documents thoroughly, the version 2 format speaking the netplan-style vocabulary that covers bonds, bridges and VLAN tags, and the site's network-config templates live beside the seed generator in the infrastructure repository, one template per network role, the deployment choosing its role the way it chooses its hostname.
The template update lifecycle
A template that ages is a slow-motion drift, every deployment inheriting last quarter's packages and last year's decisions, so the template carries its own lifecycle with a cadence. The monthly rebuild is the standard rhythm, the build pipeline, virt-customize over the fresh cloud image with the current package list, or the in-place update of the existing template through a deliberate boot-and-upgrade cycle, producing a new versioned artifact, site-template-2026-11.qcow2, with the old one kept for the rollback window.
The clones inherit the new baseline at their own pace, and the fleet's redeployment policy decides the pace: the rolling pattern redeploys one machine per role per week through the same one-command flow, the machines cycling onto the current template within a month, while the on-redeploy pattern lets each machine carry its baseline until its next scheduled rebuild, trading currency for stability. Both patterns share the verification discipline from the deployment section, every redeployment passing the same three checks, because a template change is a deployment like any other and the checkpoints are what keep the pattern honest through the versions.
The quarterly audit closes the loop, a diff of the template's package list against the fleet's requirements, a boot test of a fresh clone, and the retirement of any template version whose last clone has moved on, the artifact lifecycle as tidy as the deployment lifecycle. The fleet that runs this rhythm has a property worth stating plainly: its configuration is never more than a month old, its machines are never more than a redeployment away from the current baseline, and the whole mechanism is one command, one seed and one template, the smallest moving parts that a reproducible infrastructure can be built from.
The closing perspective
The template-and-seed pattern is the private-infrastructure answer to the cloud's elasticity, and the mechanics cost less than an afternoon to stand up on any host this series has already built. The cloud-init engine is the same one the public providers run, the NoCloud seed is a volume-label convention, and the deployment workflow is a command with two disks and a network. The discipline that makes the pattern pay is the repository: the template build files, the seed generator and the network templates all versioned together, so that the fleet's entire personality, every machine that exists and every machine that could exist next, is an artifact that fits in a git log and a review, which is the quiet end state of every automation story this series has told.