Two-node print server stack with nightly cloud-init reprovisioning. USB printers plug into a Raspberry Pi 4B, which exports them over the network via USB/IP. An Incus LXC container on a separate host picks them up and serves them via CUPS.
USB Printers ──► Raspberry Pi 4B LAN
(usbipd, TCP 3240) ────────────► Incus LXC container
[pi-bootstrap.sh] (CUPS + nginx/TLS)
[printserver-bootstrap.sh]
Both nodes reprovision nightly from cloud-init: the container is destroyed and recreated; the Pi reboots and re-applies config. Neither node accumulates state.
- Runs Ubuntu 26.04 Server (arm64)
usbipdlistens on TCP 3240 and exports all attached USB devicesusbip-bind-allautomatically binds every connected printer at boot- Ethernet (
eth0) is the primary interface (lower route-metric); WiFi (wlan0) is secondary and optional - All packages pre-installed into the SD card image before first boot — first boot requires no internet
- Nightly
reprovision.timerrunscloud-init clean --logs --rebootat 02:00
- Ubuntu 26.04 LXC container on an Incus host
- CUPS serves IPP/LPD/Bonjour to the LAN
- Optional nginx + Let's Encrypt TLS proxy on port 443
- USB printers attached via
usbip attachat provisioning time; auto-discovered and registered withlpadmin - Printer
lpadmincommands baked back into cloud-init after first discovery — nightly reprovisioning re-registers all printers without re-running discovery - Based on a pre-built Incus image (
printserver-base) with all packages installed — container reprovisioning requires no internet
.
├── shared.env.example # SSH keys, LAN subnet, Pi hostname (shared by both scripts)
├── pi-bootstrap.env.example # SD card device, WiFi password
├── pi-bootstrap.sh # Flash SD card and write cloud-init for the Pi
├── printserver-bootstrap.env.example
├── printserver-bootstrap.sh # Provision the CUPS LXC container via Incus
├── printserver-image-build.sh # Build the pre-baked printserver-base Incus image
└── cloud-init/ # Generated output (gitignored); inspect after a run
| Tool | Purpose |
|---|---|
bash, coreutils |
Scripts |
pv, xz-utils, curl |
SD card flashing |
systemd-container (systemd-nspawn) |
Chroot into arm64 image for pre-install |
incus CLI |
Container management |
gh (optional) |
GitHub operations |
Missing flash dependencies are installed automatically by pi-bootstrap.sh.
- Incus installed and listening on HTTPS (
incus remote add ...) - SSH access from the management machine (same name as the Incus remote)
- A storage pool available (default:
incus-pool) eno1(or configuredPARENT_IFACE) for MACVLAN
cp shared.env.example shared.env
cp pi-bootstrap.env.example pi-bootstrap.env
cp printserver-bootstrap.env.example printserver-bootstrap.env
chmod 600 shared.env pi-bootstrap.env printserver-bootstrap.env
$EDITOR shared.env # SSH keys, LAN subnet, Pi hostname
$EDITOR pi-bootstrap.env # DEVICE=/dev/sdX, WIFI_PASSWORD=
$EDITOR printserver-bootstrap.env # INCUS_REMOTE=, MAC_ADDRESS=Insert the SD card, identify the device (lsblk), then:
sudo ./pi-bootstrap.sh --flash --forceThis will:
- Download Ubuntu 26.04 arm64 raspi image if not already present (SHA256 verified)
- Flash the image to the SD card
- Pre-install packages via
systemd-nspawn(no internet needed on first boot):usbutils,ufw,wireless-regdb,zstd,iw,linux-tools-common, andusbip/usbipdextracted fromlinux-raspi-tools - Patch the brcmfmac NVRAM with
ccode=US(enables 5GHz channels) - Write cloud-init
user-data,user-data.nightly, andmeta-datato the boot partition
Insert the SD card into the Pi and power on. Cloud-init runs automatically on first boot.
Re-flashing an already-configured card: --flash forces a full OS flash. Without it, the script only updates cloud-init files if the partition is already Ubuntu.
Set ENABLE_VIRTUAL_PRINTERS=3 in pi-bootstrap.env to create 3 virtual USB printers via dummy_hcd + USB gadget configfs. Useful for testing without physical hardware.
Run once before first deployment. Creates a pre-baked Incus image named printserver-base with CUPS, usbip tools, nginx, certbot, and printer drivers installed:
./printserver-image-build.sh
# Rebuild: ./printserver-image-build.sh --force./printserver-bootstrap.shThis will:
- Create a MACVLAN network and Incus profile if not present
- Launch the
printservercontainer fromlocal:printserver-base - Wait for cloud-init to finish
- Wait for
usbip attachto succeed (Pi must be up and reachable) - Discover all attached USB printers and register them in CUPS via
lpadmin - Bake the
lpadmincommands back into the nightly cloud-init config - Push the nightly cloud-init to the Incus host and install a systemd timer for 02:00 reprovisioning
Re-provisioning from scratch: --reprovision destroys the existing container first.
| Node | Mechanism | Time |
|---|---|---|
| Pi | reprovision.timer → cloud-init clean --logs --reboot |
02:00 |
| Printserver | systemd timer on Incus host → destroy + recreate container | 02:00 |
The nightly configs are internet-free — all packages are pre-installed in the base images. Reprovisioning will succeed during a connectivity outage.
Both nodes use ufw. Allowed inbound:
| Node | Port | Protocol | Source |
|---|---|---|---|
| Pi | 3240 | TCP | LAN (PRINT_CIDRS) |
| Pi | 22 | TCP | SSH_CIDRS (or any if unset) |
| Printserver | 631 | TCP | LAN (PRINT_CIDRS) |
| Printserver | 22 | TCP | SSH_CIDRS (or any if unset) |
| Printserver | 443 | TCP | any (if TLS enabled) |
Set in printserver-bootstrap.env:
ENABLE_LETSENCRYPT=true
LE_EMAIL=admin@example.com
CONTAINER_FQDN=printserver.example.com
NAMECHEAP_API_USER=myusername
NAMECHEAP_API_KEY=abc123...
NAMECHEAP_CLIENT_IP=203.0.113.10 # IP of the management machine, whitelisted in Namecheap APICertificates are issued via DNS-01 challenge using the Namecheap XML API — no inbound ports 80 or 443 need to be open. nginx terminates TLS on port 443 and proxies to CUPS on localhost:631. Certificates are stored in a persistent Incus storage volume (printserver-letsencrypt) and survive nightly container reprovisioning.
CONTAINER_FQDN must resolve to the container's IP and its domain must be managed by Namecheap. Enable API access at namecheap.com → Profile → Tools → API Access and whitelist NAMECHEAP_CLIENT_IP.
Pi won't connect to WiFi
- Confirm
WIFI_PASSWORDinpi-bootstrap.envand re-flash. - 5GHz band: verify the brcmfmac NVRAM patch ran (
brcmfmac NVRAM patched with ccode=USin bootstrap output).
cloud-init didn't run (Pi)
- Check
ds=nocloudis in the kernel cmdline:cat /boot/firmware/current/cmdline.txt - Check instance-id changed:
cat /boot/firmware/meta-data
usbip attach fails
- Pi must be up and
usbipdlistening:nc -zv usbproxy.ancapistan.io 3240 - USB printers must be connected to the Pi before boot.
CUPS shows printers offline
- Re-run
printserver-bootstrap.sh(non-destructive unless--reprovision) to re-run discovery and re-register printers.