A modern, interactive TUI "installer" for Flatcar Container Linux, designed for bare-metal deployments. Not a real installer because making one would be dumb. There's no reason to make a "Bluefin Server" if Bluefin just gives you a nice tool to use Flatcar. But also it's just an ignition thing, so let's UNIFY COREOS AND FLATCAR INSTALLATION and share one installer and tests.
Knuckle is a charm.sh form that makes a valid ignition file and passes it to the Flatcar installer. We're just making users not have to use ignition, with an ubuntu-server style install UX.
This is also basically Azure Container Linux (Home Edition).
- Download an ISO for ARM or AMD64 and install it.
- Follow the installer you'll be left with a pristine upstream install of Flatcar linux
- This is early and prealpha, all feedback and contributions welcome!
Warning
Pre-alpha software — use with caution
- Wipes the target disk — the installer is destructive with no undo after confirmation
- UEFI-only — BIOS/legacy boot is not supported
- Single disk only — multi-disk setups are not supported
- English only — no i18n support yet
Knuckle is pre-alpha. Test on hardware you can reinstall.
From Releases, download:
| Architecture | Release asset |
|---|---|
| AMD64 / x86_64 | knuckle-installer-stable-amd64.iso |
| ARM64 | knuckle-installer-stable-arm64.iso |
Verify the checksum (recommended):
# use the matching .sha256 file for your architecture
sha256sum -c knuckle-installer-stable-amd64.iso.sha256Linux:
# Replace /dev/sdX with your USB device (check with lsblk first)
sudo dd if=knuckle-installer-stable-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsyncmacOS: use diskutil list to find your USB device and write with dd using the matching /dev/diskN path.
Windows: use Rufus, select the ISO, choose DD Image mode, then start.
- Insert the USB drive into the target machine.
- Enter BIOS/UEFI setup (commonly
F2,F12, orDelat power-on). - Set USB as first boot device, or pick it from the one-time boot menu.
- Save and reboot; Knuckle starts automatically.
The base flow is 9 steps:
- Welcome (
Ctrl+Atoggles external Ignition URL mode) - Network (DHCP or static IPv4)
- Storage (select target disk; all data is erased)
- User (hostname, timezone, password, SSH keys)
- Sysext (optional Flatcar Bakery extensions)
- Update strategy
- Review (
Ctrl+Btoggles full Butane preview) - Install (
flatcar-installruns with live progress) - Done (remove USB and reboot)
If you enable Tailscale or NVIDIA options, additional wizard steps appear.
SSH to the installed system with the credentials you configured:
ssh core@<your-server-ip>For unattended installs, see docs/HEADLESS-CONFIG.md.
Another Linux for the home? Flatcar is in the CNCF, which means there's a core operating system in a vendor-neutral Foundation. The kind of tech that will stick around in the long term. And designed to run anything you want. Plop anything from linuxserver.io right on it for an awesome setup.
Perfect for home servers, NAS builds, k8s cluster setups, you name it. The Flatcar Bakery is included, allowing for endless extensibility:
- Up to 11-step guided wizard — Welcome → Network → Storage → User → Sysext → [Nvidia] → [Tailscale] → Update Strategy → Review → Install → Done (Nvidia and Tailscale steps are conditional on sysext selection)
- Channel selector with version details — shows kernel, systemd, docker, containerd, ignition, and etcd versions per channel (sourced from SBOM JSON,
version.txt, package lists, androotfs-included-sysexts) - Hardware probing — automatic disk and network interface discovery with
/dev/disk/by-idpath resolution - Network configuration — DHCP or static IPv4
- User setup — hostname, timezone, password (bcrypt hashed), GitHub SSH key fetching with multi-key support, local
~/.ssh/*.pubauto-detection - System extensions — architecture-aware sysext catalog fetched via TLS from flatcar/sysext-bakery GitHub Releases API (not yet cryptographically verified — see docs/SECURITY.md)
- NVIDIA driver series — conditional step shown when
nvidia-runtimesysext is selected; selects the kernel driver series (e.g.550,560) matching the chosen NVIDIA runtime sysext; supported in both TUI and headless (nvidiaDriverSeries) modes - Tailscale provisioning — conditional step shown when
tailscalesysext is selected; sets a pre-auth key (tskey-auth-…) written to/etc/tailscale/tailscale.env; supported in both TUI and headless (tailscaleAuthKey) modes - Update strategy — reboot, off, or etcd-lock options
- Review screen — full Butane YAML preview before install
- Install step — progress bar, wipes stale disk signatures, runs
flatcar-install, then relocates the backup GPT header for larger target disks - Ignition generation — produces valid Ignition JSON via in-process Butane compilation (Flatcar variant, no CLI dependency)
- Headless mode —
--headless --config <file.json>for automated installs (CI/CD friendly) - Installer ISO — UEFI-bootable ISO with systemd-boot (amd64 + arm64), with
systemd.gpt_auto=0set in both boot entries to avoid GPT auto-generator hangs when the ISO is written to larger USB media - Config validation — consistency checks before install
- Swap file — 4 GiB
/var/swapfileprovisioned by default; configurable size (up to 32 GiB) or disabled via headless config - Dry-run mode —
--dry-runflag skips all disk writes - Ctrl+C double-press — confirmation before quitting
| Dimension | Supported |
|---|---|
| Architecture | x86_64, ARM64 |
| Storage | Single target disk |
| Networking | DHCP, static IPv4 |
| Language | English |
| Sysexts | Official Flatcar Bakery (via GitHub Releases API, arch-aware) |
| Config mode | Guided OR external Ignition URL (Ctrl+A) — mutually exclusive |
Ctrl+Aon the Welcome step — toggle external Ignition URL modeCtrl+Bon the Review step — toggle the Butane YAML previewCtrl+C× 2 — quit with confirmation
# Build from source (amd64)
just build
# Cross-compile for arm64
just build-arm64
# Run the installer (on a Flatcar live environment)
./bin/knuckle
# Dry-run mode (no disk writes)
./bin/knuckle --dry-run
# Headless install from JSON config
./bin/knuckle --headless --config install.json
# Pin to a specific Flatcar release
./bin/knuckle --channel beta --flatcar-version 3975.2.1| Flag | Default | Description |
|---|---|---|
--dry-run |
false | Simulate install — no disk writes |
--headless |
false | Non-interactive mode; requires --config |
--config <file.json> |
— | Path to JSON config for headless installs |
--channel <name> |
stable | Flatcar release channel: stable, beta, alpha, lts, edge |
--flatcar-version <ver> |
— | Pin to a specific Flatcar version (e.g. 3510.2.8); omit to use latest |
--log-file <path> |
/tmp/knuckle.log |
Path to structured log file (slog); stdout is reserved for the TUI |
--demo |
false | Mock hardware and catalog data — no network, no real disks; for UI demos and screen recording |
--version |
— | Print version and exit |
| Variable | Example | Effect |
|---|---|---|
KNUCKLE_ARCH |
KNUCKLE_ARCH=arm64 just vm |
Overrides the default amd64 target used by just recipes so builds, ISO tasks, and VM workflows target arm64 instead. Useful for ARM64 cross-builds and VM testing. |
KNUCKLE_TEST_MAIN |
KNUCKLE_TEST_MAIN=1 go test ./cmd/knuckle |
Internal test helper used by cmd/knuckle/main_test.go to re-enter main() inside the compiled test binary, so flag-validation and early-exit paths can be exercised without launching the TUI. |
--headless --config <file.json> drives the same install path as the TUI without any user interaction. Useful for CI/CD, automated bare-metal provisioning, and scripted lab setups.
Minimal example (install.json):
{
"hostname": "flatcar-01",
"channel": "stable",
"disk": "/dev/disk/by-id/ata-SomeSeagate_1TB",
"network": {"mode": "dhcp"},
"users": [
{"username": "core", "ssh_keys": ["ssh-ed25519 AAAA..."]}
],
"update_strategy": "reboot"
}Swap is enabled by default (4 GiB /var/swapfile). To customise:
{
"hostname": "flatcar-01",
"channel": "stable",
"disk": "/dev/disk/by-id/ata-SomeSeagate_1TB",
"network": {"mode": "dhcp"},
"users": [
{"username": "core", "ssh_keys": ["ssh-ed25519 AAAA..."]}
],
"update_strategy": "reboot",
"swap": {"enabled": true, "size_mb": 2048}
}To disable swap entirely:
{
"hostname": "flatcar-01",
"channel": "stable",
"disk": "/dev/disk/by-id/ata-SomeSeagate_1TB",
"network": {"mode": "dhcp"},
"users": [
{"username": "core", "ssh_keys": ["ssh-ed25519 AAAA..."]}
],
"update_strategy": "reboot",
"swap": {"enabled": false}
}For the full field reference see docs/HEADLESS-CONFIG.md.
Valid swap.size_mb range: 1–32768 (MiB). Omitting swap or setting size_mb: 0 uses the 4 GiB default.
just # list all recipes
just ci # full CI: tidy + fmt + vet + lint + vuln + test-race + cover + headless-e2e + build
just build # GOOS=linux GOARCH=amd64 CGO_ENABLED=0 → bin/knuckle
just build-arm64 # cross-compile arm64 → bin/knuckle-arm64
just test # go test ./...
just vuln # govulncheck ./...
just cover # coverage profile + summary
just cover-check # per-package coverage threshold gate
just headless-test # build + canned JSON config (CI gate, runs on host)just vm # real install in QEMU → auto-boots installed system after
just vm-e2e # automated: headless install → boot → verify SSH + hostname (4 passes)
just hardware-repro # installer ISO in a hardware-like VM, captures install failure artifacts
just boot-iso # build ISO → boot in QEMU GTK window
just e2e # build ISO → boot → interactive install
just ssh # SSH into running VMjust vm downloads a Flatcar stable QEMU image, boots a VM with two disks (boot + target), SCPs the binary in, and launches knuckle over SSH. After install completes, it kills the installer VM and boots from the installed target disk to verify SSH works.
just vm-e2e is fully automated — runs 4 passes (DHCP, static network, docker sysext, NVIDIA), verifying hostname, OS version, update strategy, and sysext activation on each.
just hardware-repro boots the installer ISO with q35, UEFI, a SATA target disk, and an e1000e NIC so the VM looks more like a generic physical machine than a paravirtualized guest. It then runs the same internal/install path via --headless and saves the useful artifacts under .vm/ (hardware-install-output.log, hardware-knuckle.log, hardware-journal.log, hardware-disk-inventory.log, hardware-installer-serial.log).
ARM64 VM testing: KNUCKLE_ARCH=arm64 just vm (requires native arm64 hardware or QEMU TCG). Set the same env var for other arm64-targeted just recipes such as build, vm-e2e, iso, and boot-iso.
Prerequisites:
- Go 1.26+ and just
- QEMU with KVM:
- Ubuntu/Debian:
apt install qemu-system-x86-64 qemu-efi-aarch64 ovmf - Fedora/RHEL:
dnf install qemu-system-x86 qemu-system-aarch64 edk2-ovmf - Arch:
pacman -S qemu qemu-efi-aarch64 edk2-ovmf
- Ubuntu/Debian:
- OVMF firmware — required for
just hardware-repro(auto-installed above) and UEFI VM testing - Flatcar QEMU base image — download via docs/GHOST-LAB.md before first run.
just vmwill fail if the image is missing
cmd/knuckle/ → CLI entrypoint, flag parsing, runner wiring
internal/bakery/ → sysext catalog + Flatcar release/SBOM fetchers, SHA512+GPG verification
internal/github/ → SSH key fetch + GitHub Releases API client
internal/headless/ → --headless --config JSON-driven install path
internal/ignition/ → Butane assembly + in-process Butane→Ignition compilation
internal/install/ → flatcar-install orchestration via runner
internal/iso/ → installer ISO builder helpers
internal/model/ → shared data types (InstallConfig, DiskInfo, NetworkInterface)
internal/probe/ → lsblk + ip addr JSON parsing, /dev/disk/by-id resolution
internal/runner/ → Runner interface: RealRunner, DryRunner, SpyRunner
internal/tui/ → Bubble Tea view models (one sub-model per step)
internal/validate/ → hostname, CIDR, gateway, SSH key, timezone, disk path validators
internal/wizard/ → step state machine, navigation, validation gates
- Runner abstraction — every external command goes through
internal/runner.Runner. Three implementations:RealRunner(prod),DryRunner(no-op + logging),SpyRunner(test recorder). Reboot is injected viarebootFn. - Flatcar Butane variant —
variant: flatcar, compiled in-process viagithub.com/coreos/butanev0.28+. NobutaneCLI needed on the target system. - Architecture-aware —
InstallConfig.Archis set fromruntime.GOARCH(compile-time constant). Sysext catalog, channel fetches, and ISO builds all parameterize on arch. LTS channel is guarded for arm64 (not published by Flatcar). - Sysext catalog — from flatcar/sysext-bakery GitHub Releases API; selects
x86-64.raworarm64.rawassets based on target arch. - Channel versions — assembled from SBOM JSON (preferred),
version.txt, package lists, androotfs-included-sysexts. - Supply-chain verification — SHA512 digest check + GPG signature verification against embedded Flatcar signing key.
- Disk identity —
/dev/disk/by-idpreferred; never trusts/dev/sdXenumeration order. - Headless mode — mirrors TUI path through the same
internal/installpackage. JSON config schema witharchfield. - ISO injection — modifies Flatcar's
usr.squashfsdirectly (the only reliable method for Flatcar PXE live boot). Uses systemd-boot (UEFI-only, BLS entries).
- Go 1.26+ (CGO_ENABLED=0, static binary)
- Bubble Tea v2.0.6 — TUI framework
- Lip Gloss v2.0.3 — styling
- Huh v2.0.3 — form inputs (Dracula theme)
- Bubbles v2.1.0 — reusable components
- Butane v0.28 — Ignition config compilation (in-process)
- ProtonMail/go-crypto — GPG signature verification
- flatcar-install — disk provisioning
- CI (
ci.yml) — go mod tidy, gofmt, vet, golangci-lint, govulncheck, test -race, per-package coverage gate, headless e2e, arm64 cross-compile - Security (
security.yml) — CodeQL (Go), OSSF Scorecard, dependency-review - Release (
release.yml) — triggered onv*tags; builds amd64 onubuntu-latest, arm64 onubuntu-24.04-arm; produces binaries + installer ISOs + cosign bundles
Once knuckle finishes, the machine reboots into a running Flatcar system. Here's what to do next.
Log in — SSH as the core user with the key(s) you provided during setup:
ssh core@<ip-or-hostname>No password login by default; the key you entered (or fetched from GitHub) is the only way in.
Check the system — Flatcar is an immutable OS. The root filesystem is read-only; user state lives under /var, /etc, and /home. To see what's running:
systemctl list-units --type=service
journalctl -b # boot logs
cat /etc/os-release # OS versionSystem extensions (sysexts) — if you installed Docker, Kubernetes, or other sysexts, they land in /etc/extensions/ and activate on the next boot (or immediately via systemd-sysext refresh). Check status:
systemd-sysext status
docker version # if docker sysext was installedUpdates — Flatcar auto-updates by default. The update strategy you chose controls how reboots happen:
| Strategy | Behaviour |
|---|---|
reboot |
Reboots automatically when an update lands |
etcd-lock |
Co-ordinates reboots across a cluster via locksmith |
off |
Downloads updates but never reboots — you reboot manually |
Check update status: update_engine_client -status
Install more software — the read-only root means traditional package managers don't apply. Options:
- Sysexts — system-level tools (Docker, Kubernetes, Tailscale, NVIDIA) via the Flatcar Bakery
- Containers — run workloads with Docker or another container runtime
- Toolbox / distrobox —
toolbox enterdrops you into a mutable Fedora container for ad-hoc CLI work
Reprovisioning — to change config (add users, switch update strategy, add sysexts), edit the Ignition/Butane source and re-run knuckle. Flatcar is designed for reprovisioning rather than in-place mutation.
- Flatcar on Discord — chat with the Flatcar community
- Flatcar docs — official documentation
- Troubleshooting guide — common install/first-boot fixes and diagnostic commands
- knuckle issues — file bugs and ideas
Apache 2.0


