Building a Reusable Alpine LXC Template on Proxmox | Felipe MartinBuilding a Reusable Alpine LXC Template on Proxmox
July 22, 2026#proxmox<br>#homelab<br>#alpine<br>#lxc
I run most of my homelab services as LXC containers on Proxmox, and lately I’ve been reaching for Alpine Linux because the base image is tiny. The stock alpine-3.23-default template Proxmox ships is great for that reason, but it comes with a catch: it has no SSH server installed . A fresh Alpine container boots with no sshd, so any automation that expects to connect over SSH (in my case Terraform provisioning the container and Ansible configuring it) has nothing to talk to on first boot.<br>You can fix this by hand every time you create a container, but that gets old fast. A better option is to bake your own template once, with SSH already enabled, and reuse it for every new container. Proxmox makes this straightforward because a container backup (vzdump) can be dropped into the template cache and used as an OS template directly.<br>The plan is:<br>Create a throwaway container from the stock Alpine template.<br>Install and enable sshd (plus anything else you want baked in).<br>Wipe the machine-specific bits so every clone comes up unique.<br>Dump it to the template cache with vzdump and rename it.<br>Everything below runs on the Proxmox node as root. I’ll use VMID 9000 for the scratch container — pick any free ID on your cluster.<br>Step 1: Create a scratch container (Proxmox Host)<br>Create a container from the stock template with a temporary network interface so we can reach the package repositories:<br>pct create 9000 local:vztmpl/alpine-3.23-default_20260116_amd64.tar.xz \<br>--hostname alpine-tmpl --arch amd64 --ostype alpine \<br>--cores 1 --memory 512 --rootfs local-zfs:2 \<br>--net0 name=eth0,bridge=vmbr0,ip=dhcp --unprivileged 1
pct start 9000
Adjust --rootfs and --net0 to match your storage and bridge. Give it a moment to pick up a DHCP lease before continuing.<br>Step 2: Install and enable SSH (LXC)<br>Alpine uses OpenRC rather than systemd, so enabling a service at boot is rc-update add default. Rather than stringing everything into one pct exec one-liner, drop into an interactive shell with pct enter and run each command on its own so it’s easier to follow along:<br>pct enter 9000
You’re now inside the container as root. Refresh the package index first:<br>apk update
Install OpenSSH plus a Python interpreter:<br>apk add --no-cache openssh openssh-server python3
python3 is optional. I install it because my Ansible playbooks need a Python interpreter on the target, and baking it in saves a slow package install on the first run. Leave it out if you want the smallest possible template.
Enable sshd at boot:<br>rc-update add sshd default
Without this, sshd is installed but never starts on the next boot — Alpine doesn’t start services just because they’re installed.<br>Create the sshd drop-in directory:<br>install -d /etc/ssh/sshd_config.d
Write the template’s SSH policy:<br>printf "MaxAuthTries 30\nPermitRootLogin prohibit-password\n" \<br>> /etc/ssh/sshd_config.d/10-template.conf
MaxAuthTries 30 saves me a headache: my SSH agent offers several keys on connect, and the default limit of 6 trips a “too many authentication failures” error before it gets to the right one. Bumping the limit avoids that during automated provisioning.<br>PermitRootLogin prohibit-password keeps root login key-only, which is how my provisioning tooling bootstraps the container before it creates a regular admin user.<br>Step 3: Wipe the machine-specific state (LXC)<br>This is the step that’s easy to forget and important to get right. If you dump the container as-is, every clone made from it will share the same SSH host keys and machine ID which is both a security problem and a source of very confusing bugs.<br>Remove the SSH host keys:<br>rm -f /etc/ssh/ssh_host_*
This is safe: Alpine’s sshd init script runs ssh-keygen -A on start if the host keys are missing, and Proxmox regenerates them when it creates a container from the template.<br>Clear the machine ID:<br>: > /etc/machine-id
Each clone needs its own machine ID; leaving the scratch container’s ID in place would make every container created from this template report the same one.<br>Remove any authorized keys that leaked in during testing:<br>rm -f /root/.ssh/authorized_keys
This keeps the template clean the real keys get injected fresh when you create the actual container.<br>Clear the package cache to keep the template small:<br>rm -rf /var/cache/apk/*
Leave the container shell:<br>exit
Step 4: Dump it to the template cache (Proxmox Host)<br>Stop the container and strip its network interface so the scratch MAC address doesn’t leak into the template:<br>pct stop 9000<br>pct set 9000 --delete net0
Then dump it straight into the template cache directory, compressed with zstd:<br>vzdump 9000 --mode stop --compress zstd \<br>--dumpdir /var/lib/vz/template/cache/
vzdump writes a file named like vzdump-lxc-9000-.tar.zst. Rename it to...