Part 1: Image Mode and the itg-base Golden Image
What image mode actually is
Building a server in Fedora bootc feels like building a container, because it basically is one. The whole OS is an image you build with a Containerfile, push to a registry, and roll out. The mental model: the image is a cake recipe, every host bakes the exact same cake, and the only per-host difference is the crumbs each one drops on its own plate.
The three rules that drive every decision:
/usris immutable and comes straight from the image. You cannot write to it at runtime./etcis a 3-way merge. Your local edits survive upgrades, which is a blessing and occasionally a curse./varis machine-local and never overwritten by the image. State lives here.
Because /usr is read-only, you do not "install and configure" at runtime. You bake configuration into the image with drop-ins, sysusers.d, tmpfiles.d, and build-time systemctl enable.
What lives in itg-base
FROM quay.io/fedora/fedora-bootc:latest. Then:
Users, via sysusers.d, not useradd. I declare ansible (1000), worker (1001), and itguyeric (2001) as system users in a sysusers file. Gotcha that cost me time: group lines (g) must come before the user lines (u) that reference them, or the build fails.
Passwordless wheel sudo via a drop-in, and the root password block stays commented out. This box is keys-only.
SSH: hardened sshd config plus baked-in public keys. I cannot drop authorized_keys into a home directory at build time because homes live in /var. So the public keys get COPY'd into /usr/share/ssh-keys/, and a tmpfiles.d rule materializes them into each user's ~/.ssh/authorized_keys on first boot. Public keys are safe to commit; private keys and real secrets never go in the image.
Packages. dnf install for the fleet baseline: cockpit-*, nfs-utils, podman, qemu-guest-agent, tuned, vim-enhanced (and later rsync, added in one line for the whole fleet). I also dnf remove the stuff I never want on a VM: iscsi, kdump, kexec.
Quality-of-life: tuned enabled with a profile, the timezone pinned with ln -sf /usr/share/zoneinfo/America/Chicago /etc/localtime, and an /etc/environment that sets EDITOR and SYSTEMD_EDITOR=vim so systemctl edit stops dumping me into nano.
A NetworkManager hostname-mode=none drop-in (the reason is Part 3), a loglevel=3 kernel arg via kargs.d, and a drop-in that sets the bootc auto-update timer schedule (Part 7).
The enable line. One build-time systemctl enable covers podman.socket sshd systemd-journald-audit.socket tuned and the shared NFS mount. Note what is NOT here: I do not enable the auto-update timer in the base (that is per-host), and you never systemctl enable a Quadlet-generated service.
Lessons
- Configuration is data baked into the image, not commands run on a live box. If you find yourself wanting to SSH in and edit
/usr, you are fighting the model. - Group-before-user in sysusers.
- Public keys in the image are fine. Secrets are not. That line matters and it holds for the whole series.
Runbooks
The build
- Image Mode & Base
- Storage
- Hostnames & DNS
- Registry
- Actions Runner
- The Pipeline
- Nightly Auto-Deploy
- First Workload
- VSCode Cockpit
- Hugo Auto-Deploy
- Cloudflare & Kobo
- Runner Provisioning
- Tailscale Router
- SWAG & Website
- Split-DNS
- Plex
- Media Library Support
- The Day After
- Self-Hosted Media
- Hypervisor Joins the Fleet
- The Matrix Homeserver
Reference