From fa50de97fd8198b6f5523f70c0288e37c8790bef Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 10:38:16 +0000 Subject: [PATCH 01/19] feat: add live box ISO (hosts/live) + Makefile build targets Adds a live 'Box' ISO that boots the same configured system as a disk install (KDE, Coder server, k3s, Podman, bundled templates) entirely from RAM, with admin bootstrap + template deploy on first boot. - nixos/live-iso.nix: imports nixpkgs iso-image.nix + all-hardware; forces off systemd-boot/EFI-var writes (ISO carries its own loader); bakes the flake at /etc/nixos-repo so Coder bootstrap finds coderd/ templates; autologin + Coder admin defaults. BIOS boot gated to x86 (syslinux) so the module also builds for aarch64 (EFI-only). - hosts/live/default.nix: new 'live' host (nixosConfigurations.live), imports only nixos/live-iso.nix; no disko/facter/hardware-config. Independent of the install.sh flow. - Makefile: 'make live-iso' (native) and 'make live-iso/' (overrides nixpkgs.hostPlatform via extendModules). - README.md/agents.md: document the live ISO and build targets. Closes #5 --- Makefile | 34 ++++++++++++++ README.md | 55 ++++++++++++++++++++++ agents.md | 3 ++ hosts/live/default.nix | 23 +++++++++ nixos/live-iso.nix | 104 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 219 insertions(+) create mode 100644 Makefile create mode 100644 hosts/live/default.nix create mode 100644 nixos/live-iso.nix diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..25d9df4 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +# Coder box — build targets for the live "Box" ISO (hosts/live). +# +# Requires Nix with flakes enabled (nix-command + flakes). +# +# make live-iso # build the live ISO for this machine's arch +# make live-iso/x86_64-linux # build an x86_64 live ISO +# make live-iso/aarch64-linux # build an aarch64 live ISO (EFI-only) +# +# Short arch names work too: `make live-iso/aarch64`, `make live-iso/x86_64`. +# Cross-arch builds need a suitable builder (native, a remote builder, or +# binfmt emulation); without one Nix will refuse to build a non-native ISO. +# +# The finished image lands in ./result/iso/coder-box-live-*.iso (the build +# prints the out-path). Flash it with e.g. +# sudo dd if=result/iso/coder-box-live-*.iso of=/dev/sdX bs=4M status=progress oflag=sync + +NIX ?= nix +FLAKE ?= . +ISO_ATTR = config.system.build.isoImage + +.PHONY: live-iso +live-iso: + $(NIX) build '$(FLAKE)#nixosConfigurations.live.$(ISO_ATTR)' --print-out-paths + +# Architecture-specific live ISO. `$*` is the part after the slash, e.g. +# "aarch64-linux" (or the short "aarch64"). We override nixpkgs.hostPlatform on +# the `live` host via extendModules so the same config builds for the requested +# architecture without adding a separate flake output per arch. +.PHONY: live-iso/% +live-iso/%: + @arch="$*"; case "$$arch" in *-linux) ;; *) arch="$$arch-linux" ;; esac; \ + echo "Building live ISO for $$arch ..."; \ + $(NIX) build --impure --print-out-paths --expr \ + "let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.live.extendModules { modules = [ { nixpkgs.hostPlatform = \"$$arch\"; } ]; }).$(ISO_ATTR)" diff --git a/README.md b/README.md index f0390cc..151bce2 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ NixOS configuration for Coder demo and workshop boxes. flake.nix # entry point: nixosConfigurations. per machine flake.lock # pinned nixpkgs / disko / nixos-facter-modules configuration.nix # shared NixOS config (all machines) +Makefile # `make live-iso` / `make live-iso/` build targets local.nix.example # template copied to hosts//local.nix by install.sh .gitignore # ignores hosts/*/local.nix nixos/ @@ -38,6 +39,7 @@ nixos/ k3s-sysbox.nix # k3s + sysbox-runc runtime class k3s-podman.nix # k3s + rootless Podman socket screenconnect.nix # optional ScreenConnect remote access client + live-iso.nix # turns the shared config into a bootable live ISO (hosts/live) pkgs/ coder.nix # custom Coder server package coderd-provider.nix # terraform-provider-coderd package @@ -49,6 +51,8 @@ hosts/ local.nix # gitignored: admin creds, secrets, SSH users templates/ nook-android/ # Workspace: build trmnl-nook-simple-touch APK + live/ # `live` host: builds the live "Box" ISO (no disk install) + default.nix # imports nixos/live-iso.nix only (no disko/facter/hardware-config) coderd/ main.tf # manages all Coder templates via coderd Terraform provider templates/ @@ -108,6 +112,57 @@ The installer generates `hosts//{default.nix,local.nix,facter.json}`, > ``` > And use a BIOS-compatible disko layout instead of `disko-standard.nix`. +## Live ISO (The Box™ on a USB — no install) + +Sometimes you don't want to wipe a disk; you just want The Box™. The `live` +host builds a bootable ISO that runs the *exact same* configured system — +KDE Plasma, the Coder server, k3s, Podman, the bundled templates — straight +from RAM, with admin bootstrap and template deploy happening on boot just like +a real install. It is not an installer. + +Build the ISO (needs a Linux machine with Nix + flakes): + +```sh +make live-iso # build for this machine's architecture +# or pick an architecture explicitly: +make live-iso/x86_64-linux +make live-iso/aarch64-linux # EFI-only (cross-arch needs a matching builder) + +# equivalent to the default target, without make: +nix build .#nixosConfigurations.live.config.system.build.isoImage +# → result/iso/coder-box-live-*.iso +``` + +Flash it to a USB stick (replace `sdX` with your device) and boot it: + +```sh +sudo dd if=result/iso/coder-box-live-*.iso of=/dev/sdX bs=4M status=progress oflag=sync +``` + +On boot it autologins to the `coderbox` desktop and Coder comes up at +`http://live.local:3000` (or the `*.try.coder.app` tunnel URL in `/etc/motd`), +with admin `admin@coder.com` / `PleaseChangeMe1234`. Everything lives in the +tmpfs overlay, so changes are discarded on reboot. + +How it's wired (see [`nixos/live-iso.nix`](nixos/live-iso.nix)): + +- `hosts/live/default.nix` imports `nixos/live-iso.nix` only — **no** + `disko-standard.nix`, `hardware-configuration.nix`, or `facter.json`. The + live root filesystem is the squashfs + tmpfs overlay from nixpkgs' + `iso-image.nix`, so there's no partition to format or mount. +- The shared `systemd-boot` / EFI-variable settings from `configuration.nix` + are forced off; the ISO carries its own GRUB-EFI + isolinux loader. +- The flake source is baked in at `/etc/nixos-repo` so the Coder bootstrap + units find `coderd/` templates and deploy them exactly like an install. + +This host is completely separate from the disk-install flow above +(`nixos/install.sh`, `disko`, `nixos-facter`); adding it changes nothing for +normal installs. To customize the live image (admin creds, SSH users, etc.), +drop a gitignored `hosts/live/local.nix` (same shape as `local.nix.example`). + +To change the baked-in admin/login defaults before sharing the ISO, override +them in `hosts/live/local.nix` or edit `nixos/live-iso.nix`. + ## After install The installer auto-creates the admin user, mints a long-lived API token to diff --git a/agents.md b/agents.md index 99a8a5d..3a3f41b 100644 --- a/agents.md +++ b/agents.md @@ -184,10 +184,13 @@ sudo k3s kubectl describe pod -n coder-workspaces k3s-sysbox.nix # k3s + sysbox runtime k3s-podman.nix # k3s + rootless Podman socket screenconnect.nix # ScreenConnect remote access client + live-iso.nix # live "Box" ISO module (imported by hosts/live) pkgs/ coder.nix # Coder server package derivation coderd-provider.nix # terraform-provider-coderd derivation hosts/ + live/ # `live` host: builds the live "Box" ISO; no disko/facter/hardware-config + # build: nix build .#nixosConfigurations.live.config.system.build.isoImage coder-thinkcentre/ # folder name = hostname; default.nix has a hardware-model header comment default.nix # host module: imports facter/legacy + local.nix + thinkcentre-only services hardware-configuration.nix # legacy fallback (used until facter.json exists) diff --git a/hosts/live/default.nix b/hosts/live/default.nix new file mode 100644 index 0000000..c7e0714 --- /dev/null +++ b/hosts/live/default.nix @@ -0,0 +1,23 @@ +# Live "Box" ISO host — "it's just The Box™", not an installer. +# +# Folder name = nixosConfigurations attribute (see flake.nix host +# auto-discovery), so this host is exposed as `nixosConfigurations.live`. +# Build the bootable ISO with: +# +# nix build .#nixosConfigurations.live.config.system.build.isoImage +# # → result/iso/coder-box-live-*.iso +# +# Unlike the install hosts (coder-thinkcentre, qemu-arm64), this host does NOT +# import nixos/disko-standard.nix, hardware-configuration.nix, or facter.json: +# the live root filesystem is the squashfs + tmpfs overlay provided by +# nixos/live-iso.nix. All of the live-specific wiring lives in that module. +# +# This host is independent of nixos/install.sh and never participates in the +# disk-install flow; adding it changes nothing for disko/nixos-install installs. + +{ lib, ... }: + +{ + imports = [ ../../nixos/live-iso.nix ] + ++ lib.optional (builtins.pathExists ./local.nix) ./local.nix; +} diff --git a/nixos/live-iso.nix b/nixos/live-iso.nix new file mode 100644 index 0000000..89f7348 --- /dev/null +++ b/nixos/live-iso.nix @@ -0,0 +1,104 @@ +# Live "Box" ISO module — "it's just The Box™", not an installer. +# +# Turns the shared Coder box configuration (configuration.nix) into a bootable +# live ISO that runs entirely from the USB/CD + RAM, with no disk install. When +# you boot this image you get the exact same system the on-disk install +# produces: KDE Plasma, the Coder server, k3s, rootless Podman, the bundled +# workspace templates, etc. — all started up automatically, just as if it were +# the disk you installed to. +# +# Build the ISO (folder name `live` => nixosConfigurations.live, see flake.nix): +# +# nix build .#nixosConfigurations.live.config.system.build.isoImage +# # → result/iso/coder-box-live-*.iso (flash with `dd`, Ventoy, etc.) +# +# This module is imported only by hosts/live/default.nix and is completely +# independent of the regular disk-install flow (nixos/install.sh, disko, +# nixos-facter). It imports NO disko / hardware-configuration.nix / facter.json: +# the live root filesystem is the squashfs + tmpfs overlay that nixpkgs' +# iso-image.nix sets up, so there is no on-disk partition to format or mount. +# +# Why the boot-loader overrides below: configuration.nix targets installed UEFI +# machines (systemd-boot + writing EFI variables). A live ISO is booted via the +# GRUB-EFI / isolinux loader baked into the image by iso-image.nix instead, so +# we force the installed-machine boot settings off to avoid eval conflicts and +# pointless EFI-variable writes on the host. + +{ config, lib, pkgs, modulesPath, self, inputs, ... }: + +{ + imports = [ + # Core ISO builder: squashfs nix store, tmpfs overlay root, kernel/initrd, + # and the EFI + BIOS ISO bootloader. Provides the `system.build.isoImage` + # attribute and the `isoImage.*` options used below. + (modulesPath + "/installer/cd-dvd/iso-image.nix") + # Broad driver/firmware set so the ISO boots on arbitrary real hardware + # (this replaces the per-host facter.json / hardware-configuration.nix that + # installed hosts rely on). + (modulesPath + "/profiles/all-hardware.nix") + ]; + + # ── ISO image settings ────────────────────────────────────────────────────── + isoImage.makeEfiBootable = true; # boot on UEFI machines + # Legacy BIOS boot uses syslinux, which is x86-only. Enable it just for x86 + # so the same module also evaluates/builds for an aarch64 live ISO (which + # boots via EFI only). isx86 covers both i686 and x86_64. + isoImage.makeBiosBootable = pkgs.stdenv.hostPlatform.isx86; + isoImage.makeUsbBootable = true; # `dd` straight to a USB stick and boot + isoImage.volumeID = "BOX_LIVE"; + # Prefix of the generated file name (result/iso/--.iso). + # `image.baseName` is the post-25.05 replacement for `isoImage.isoBaseName`. + # iso-image.nix already sets it ("nixos--"), so override with + # mkForce to win over that definition. + image.baseName = lib.mkForce "coder-box-live"; + + # ── Boot loader: let iso-image.nix own it ──────────────────────────────────── + # configuration.nix sets these for installed UEFI machines; force them off so + # they don't conflict with the image's own bootloader or try to touch the + # host's EFI variables when the live system activates. + boot.loader.systemd-boot.enable = lib.mkForce false; + boot.loader.efi.canTouchEfiVariables = lib.mkForce false; + + # ── Bake the repo into the image at /etc/nixos-repo ────────────────────────── + # The on-disk installer copies the working tree to /etc/nixos-repo; the Coder + # bootstrap units (coder-init-admin.service, the coder-template-sync + # activation script) read templates from /etc/nixos-repo/coderd and the + # locally-packaged provider mirror. Point /etc/nixos-repo at the flake source + # baked into the ISO so the live box deploys templates exactly like an + # installed box. (The git-commit lookups in those scripts fall back to + # "unknown" when no .git is present, which is fine.) + environment.etc."nixos-repo".source = self.outPath; + + # Make the pinned nixpkgs resolvable on the live box so `nix` / flake commands + # behave like an installed system, without shipping a channel. + nix.registry.nixpkgs.flake = inputs.nixpkgs; + + # ── Login + Coder admin bootstrap ──────────────────────────────────────────── + # On installed hosts these come from the gitignored hosts//local.nix + # that install.sh generates. The live image has no install step, so provide + # turn-key defaults here (same defaults the installer uses). Autologin drops + # straight into the Plasma desktop, mirroring a freshly-installed, configured + # box. Change these before handing the ISO to anyone untrusted. + services.displayManager.autoLogin = { + enable = true; + user = "coderbox"; + }; + + users.users.coderbox = { + isNormalUser = true; + description = "coderbox"; + extraGroups = [ "networkmanager" "wheel" ]; + packages = [ pkgs.kdePackages.kate ]; + initialPassword = "coderbox"; + }; + + # coder-init-admin.service reads CODER_ADMIN_* from coder.service's + # environment and creates a local admin on first boot, then mints a session + # token and deploys the templates from /etc/nixos-repo/coderd. With these set + # the live Coder instance is ready to use immediately. + systemd.services.coder.environment = { + CODER_ADMIN_EMAIL = "admin@coder.com"; + CODER_ADMIN_USERNAME = "admin"; + CODER_ADMIN_PASSWORD = "PleaseChangeMe1234"; + }; +} From 4b24ec281a8a658487c754170cc9bb8945adcbc9 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 10:55:29 +0000 Subject: [PATCH 02/19] refactor: rename live ISO target + add persistent-disk image (qcow2/raw) - Rename Makefile target live-iso -> live-ephemeral-iso (and /). - Add hosts/persistent-disk: a disko-image-builder host that produces a persistent disk image sharing disko-standard.nix's GPT layout (state survives reboots, unlike the ephemeral live ISO). - Add Makefile targets persistent-disk/qcow2 and persistent-disk/raw, each with optional / suffix. raw is dd-able to a drive; qcow2 boots in QEMU/libvirt. - Extract the shared turn-key bits (all-hardware, baked /etc/nixos-repo, autologin, Coder admin bootstrap) into nixos/box-turnkey.nix, imported by both nixos/live-iso.nix and hosts/persistent-disk. Makefile uses one extendModules helper for all targets (per-arch + imageFormat overrides). - Update README.md/agents.md. Verified: all four nixosConfigurations eval; coder-thinkcentre drv unchanged; live ISO + persistent-disk qcow2/raw eval to derivations on x86_64 and aarch64; raw disk image full build plan resolves via nix build --dry-run. --- Makefile | 78 +++++++++++++++-------- README.md | 102 ++++++++++++++++++------------ agents.md | 9 ++- hosts/persistent-disk/default.nix | 45 +++++++++++++ nixos/box-turnkey.nix | 64 +++++++++++++++++++ nixos/live-iso.nix | 95 +++++++--------------------- 6 files changed, 250 insertions(+), 143 deletions(-) create mode 100644 hosts/persistent-disk/default.nix create mode 100644 nixos/box-turnkey.nix diff --git a/Makefile b/Makefile index 25d9df4..bea19ec 100644 --- a/Makefile +++ b/Makefile @@ -1,34 +1,58 @@ -# Coder box — build targets for the live "Box" ISO (hosts/live). +# Coder box — image build targets. # -# Requires Nix with flakes enabled (nix-command + flakes). +# Requires Nix with flakes enabled (nix-command + flakes). All image builds run +# on Linux only; cross-arch builds need a matching builder (native remote +# builder or binfmt/QEMU emulation). # -# make live-iso # build the live ISO for this machine's arch -# make live-iso/x86_64-linux # build an x86_64 live ISO -# make live-iso/aarch64-linux # build an aarch64 live ISO (EFI-only) +# make live-ephemeral-iso # live ISO (tmpfs overlay; state wiped on reboot) +# make persistent-disk/qcow2 # persistent qcow2 disk image (state persists) +# make persistent-disk/raw # persistent raw disk image (dd-able to a drive) # -# Short arch names work too: `make live-iso/aarch64`, `make live-iso/x86_64`. -# Cross-arch builds need a suitable builder (native, a remote builder, or -# binfmt emulation); without one Nix will refuse to build a non-native ISO. +# Each target also takes an architecture suffix; short names are normalized to +# a *-linux triple (e.g. aarch64 -> aarch64-linux): # -# The finished image lands in ./result/iso/coder-box-live-*.iso (the build -# prints the out-path). Flash it with e.g. -# sudo dd if=result/iso/coder-box-live-*.iso of=/dev/sdX bs=4M status=progress oflag=sync +# make live-ephemeral-iso/x86_64-linux +# make persistent-disk/qcow2/aarch64-linux +# make persistent-disk/raw/aarch64 +# +# Outputs land in ./result (printed out-path). Flash a raw image or the ISO to +# a drive with e.g. +# sudo dd if=result/...img of=/dev/sdX bs=4M status=progress oflag=sync + +NIX ?= nix +FLAKE ?= . + +# Normalize an arch token to a *-linux triple: $(call norm_arch,aarch64) -> aarch64-linux +norm_arch = $(if $(filter %-linux,$(1)),$(1),$(1)-linux) + +# Single build helper used by every target. extendModules lets us override +# nixpkgs.hostPlatform (per-arch) and the disko image format from one recipe, +# so adding a flavour is just a thin target below — no duplicated nix plumbing. +# $(1) = host (nixosConfigurations.) +# $(2) = system.build. (isoImage | diskoImages) +# $(3) = extra module fields (nix attrset body, may be empty) +# $(4) = arch token (empty = builder's native arch) +define box_build + $(NIX) build --impure --no-write-lock-file --print-out-paths --expr \ + 'let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { $(if $(4),nixpkgs.hostPlatform = "$(call norm_arch,$(4))"; ) $(3) } ]; }).config.system.build.$(2)' +endef + +.PHONY: live-ephemeral-iso persistent-disk/qcow2 persistent-disk/raw -NIX ?= nix -FLAKE ?= . -ISO_ATTR = config.system.build.isoImage +# ── Live ephemeral ISO (hosts/live) ───────────────────────────────────────── +live-ephemeral-iso: + $(call box_build,live,isoImage,,) +live-ephemeral-iso/%: + $(call box_build,live,isoImage,,$*) -.PHONY: live-iso -live-iso: - $(NIX) build '$(FLAKE)#nixosConfigurations.live.$(ISO_ATTR)' --print-out-paths +# ── Persistent disk image (hosts/persistent-disk), qcow2 ───────────────────── +persistent-disk/qcow2: + $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,) +persistent-disk/qcow2/%: + $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,$*) -# Architecture-specific live ISO. `$*` is the part after the slash, e.g. -# "aarch64-linux" (or the short "aarch64"). We override nixpkgs.hostPlatform on -# the `live` host via extendModules so the same config builds for the requested -# architecture without adding a separate flake output per arch. -.PHONY: live-iso/% -live-iso/%: - @arch="$*"; case "$$arch" in *-linux) ;; *) arch="$$arch-linux" ;; esac; \ - echo "Building live ISO for $$arch ..."; \ - $(NIX) build --impure --print-out-paths --expr \ - "let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.live.extendModules { modules = [ { nixpkgs.hostPlatform = \"$$arch\"; } ]; }).$(ISO_ATTR)" +# ── Persistent disk image (hosts/persistent-disk), raw (dd-able) ───────────── +persistent-disk/raw: + $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,) +persistent-disk/raw/%: + $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,$*) diff --git a/README.md b/README.md index 151bce2..b86e9f8 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ NixOS configuration for Coder demo and workshop boxes. flake.nix # entry point: nixosConfigurations. per machine flake.lock # pinned nixpkgs / disko / nixos-facter-modules configuration.nix # shared NixOS config (all machines) -Makefile # `make live-iso` / `make live-iso/` build targets +Makefile # image build targets: live-ephemeral-iso, persistent-disk/{qcow2,raw} local.nix.example # template copied to hosts//local.nix by install.sh .gitignore # ignores hosts/*/local.nix nixos/ @@ -39,7 +39,8 @@ nixos/ k3s-sysbox.nix # k3s + sysbox-runc runtime class k3s-podman.nix # k3s + rootless Podman socket screenconnect.nix # optional ScreenConnect remote access client - live-iso.nix # turns the shared config into a bootable live ISO (hosts/live) + box-turnkey.nix # shared turn-key bits for prebuilt images (login + Coder bootstrap) + live-iso.nix # ephemeral live ISO module (hosts/live) pkgs/ coder.nix # custom Coder server package coderd-provider.nix # terraform-provider-coderd package @@ -51,8 +52,10 @@ hosts/ local.nix # gitignored: admin creds, secrets, SSH users templates/ nook-android/ # Workspace: build trmnl-nook-simple-touch APK - live/ # `live` host: builds the live "Box" ISO (no disk install) + live/ # `live` host: ephemeral live "Box" ISO (no disk install) default.nix # imports nixos/live-iso.nix only (no disko/facter/hardware-config) + persistent-disk/ # `persistent-disk` host: persistent qcow2/raw disk image + default.nix # imports disko-standard.nix + box-turnkey.nix coderd/ main.tf # manages all Coder templates via coderd Terraform provider templates/ @@ -112,56 +115,71 @@ The installer generates `hosts//{default.nix,local.nix,facter.json}`, > ``` > And use a BIOS-compatible disko layout instead of `disko-standard.nix`. -## Live ISO (The Box™ on a USB — no install) +## Prebuilt images (The Box™ without `install.sh`) -Sometimes you don't want to wipe a disk; you just want The Box™. The `live` -host builds a bootable ISO that runs the *exact same* configured system — -KDE Plasma, the Coder server, k3s, Podman, the bundled templates — straight -from RAM, with admin bootstrap and template deploy happening on boot just like -a real install. It is not an installer. +Sometimes you don't want to run the installer; you just want The Box™. Two +image flavours build the *exact same* configured system — KDE Plasma, the Coder +server, k3s, Podman, the bundled templates — with admin bootstrap and template +deploy happening on boot just like a real install. Neither is an installer. -Build the ISO (needs a Linux machine with Nix + flakes): +| Flavour | Host | State | Build | +|---|---|---|---| +| **Live ISO** (ephemeral) | `live` | tmpfs overlay — wiped on reboot | `make live-ephemeral-iso` | +| **Persistent disk** (qcow2) | `persistent-disk` | persists across reboots | `make persistent-disk/qcow2` | +| **Persistent disk** (raw) | `persistent-disk` | persists across reboots | `make persistent-disk/raw` | + +All builds need a Linux machine with Nix + flakes. Every target also takes an +architecture suffix (short names are normalized to `*-linux`); cross-arch +builds need a matching builder (native remote builder or binfmt/QEMU): ```sh -make live-iso # build for this machine's architecture -# or pick an architecture explicitly: -make live-iso/x86_64-linux -make live-iso/aarch64-linux # EFI-only (cross-arch needs a matching builder) - -# equivalent to the default target, without make: -nix build .#nixosConfigurations.live.config.system.build.isoImage -# → result/iso/coder-box-live-*.iso +make live-ephemeral-iso/aarch64-linux +make persistent-disk/qcow2/aarch64-linux +make persistent-disk/raw/x86_64 ``` -Flash it to a USB stick (replace `sdX` with your device) and boot it: +The turn-key login + Coder admin bootstrap shared by both flavours live in +[`nixos/box-turnkey.nix`](nixos/box-turnkey.nix): autologin to the `coderbox` +desktop, and admin `admin@coder.com` / `PleaseChangeMe1234`. Coder comes up at +`http://.local:3000` (or the `*.try.coder.app` tunnel URL in +`/etc/motd`). Change these before sharing an image by dropping a gitignored +`hosts//local.nix` (same shape as `local.nix.example`). + +### Live ISO (`live`) + +The live root filesystem is the squashfs + tmpfs overlay from nixpkgs' +`iso-image.nix`, so there's no partition to format or mount and **all state is +discarded on reboot**. `hosts/live/default.nix` imports +[`nixos/live-iso.nix`](nixos/live-iso.nix) (which pulls in `box-turnkey.nix`) — +**no** `disko-standard.nix`, `hardware-configuration.nix`, or `facter.json`. +The installed-machine `systemd-boot` / EFI-variable settings are forced off; the +ISO carries its own GRUB-EFI + isolinux loader (BIOS boot is x86-only, so the +aarch64 ISO is EFI-only). Flash it (it's isohybrid) and boot: ```sh sudo dd if=result/iso/coder-box-live-*.iso of=/dev/sdX bs=4M status=progress oflag=sync ``` -On boot it autologins to the `coderbox` desktop and Coder comes up at -`http://live.local:3000` (or the `*.try.coder.app` tunnel URL in `/etc/motd`), -with admin `admin@coder.com` / `PleaseChangeMe1234`. Everything lives in the -tmpfs overlay, so changes are discarded on reboot. - -How it's wired (see [`nixos/live-iso.nix`](nixos/live-iso.nix)): - -- `hosts/live/default.nix` imports `nixos/live-iso.nix` only — **no** - `disko-standard.nix`, `hardware-configuration.nix`, or `facter.json`. The - live root filesystem is the squashfs + tmpfs overlay from nixpkgs' - `iso-image.nix`, so there's no partition to format or mount. -- The shared `systemd-boot` / EFI-variable settings from `configuration.nix` - are forced off; the ISO carries its own GRUB-EFI + isolinux loader. -- The flake source is baked in at `/etc/nixos-repo` so the Coder bootstrap - units find `coderd/` templates and deploy them exactly like an install. - -This host is completely separate from the disk-install flow above -(`nixos/install.sh`, `disko`, `nixos-facter`); adding it changes nothing for -normal installs. To customize the live image (admin creds, SSH users, etc.), -drop a gitignored `hosts/live/local.nix` (same shape as `local.nix.example`). - -To change the baked-in admin/login defaults before sharing the ISO, override -them in `hosts/live/local.nix` or edit `nixos/live-iso.nix`. +### Persistent disk image (`persistent-disk`) + +Built with [disko](https://github.com/nix-community/disko)'s image builder, so +it carries the real on-disk GPT layout from `nixos/disko-standard.nix` (1 GB +ESP + ext4 root) and **state survives reboots**, exactly like a machine you ran +`install.sh` on. `hosts/persistent-disk/default.nix` imports +`disko-standard.nix` + `box-turnkey.nix`. + +- **`qcow2`** — boot it directly in QEMU/libvirt/UTM. A qcow2 is a container + format, so it can **not** be `dd`'d to a drive as-is — convert first + (`qemu-img convert -O raw box.qcow2 box.img`) or build the raw image instead. +- **`raw`** — a plain disk image you can `dd` straight onto a physical drive: + ```sh + sudo dd if=result/*.img of=/dev/sdX bs=4M status=progress oflag=sync + ``` + +Both image hosts are completely separate from the disk-install flow above +(`nixos/install.sh`, `nixos-facter`); adding them changes nothing for normal +installs. The `persistent-disk` host shares only the disk *layout* +(`disko-standard.nix`) with real installs, never the install process itself. ## After install diff --git a/agents.md b/agents.md index 3a3f41b..c0b73c8 100644 --- a/agents.md +++ b/agents.md @@ -184,13 +184,16 @@ sudo k3s kubectl describe pod -n coder-workspaces k3s-sysbox.nix # k3s + sysbox runtime k3s-podman.nix # k3s + rootless Podman socket screenconnect.nix # ScreenConnect remote access client - live-iso.nix # live "Box" ISO module (imported by hosts/live) + box-turnkey.nix # shared turn-key bits for prebuilt images (login + Coder bootstrap) + live-iso.nix # ephemeral live "Box" ISO module (imported by hosts/live) pkgs/ coder.nix # Coder server package derivation coderd-provider.nix # terraform-provider-coderd derivation hosts/ - live/ # `live` host: builds the live "Box" ISO; no disko/facter/hardware-config - # build: nix build .#nixosConfigurations.live.config.system.build.isoImage + live/ # `live` host: ephemeral live "Box" ISO; no disko/facter/hardware-config + # build: make live-ephemeral-iso (or .../) + persistent-disk/ # `persistent-disk` host: persistent qcow2/raw disk image (disko image builder) + # build: make persistent-disk/qcow2 | make persistent-disk/raw (or .../) coder-thinkcentre/ # folder name = hostname; default.nix has a hardware-model header comment default.nix # host module: imports facter/legacy + local.nix + thinkcentre-only services hardware-configuration.nix # legacy fallback (used until facter.json exists) diff --git a/hosts/persistent-disk/default.nix b/hosts/persistent-disk/default.nix new file mode 100644 index 0000000..69b12f0 --- /dev/null +++ b/hosts/persistent-disk/default.nix @@ -0,0 +1,45 @@ +# Persistent "Box" disk image host — "it's just The Box™" on a real disk. +# +# Folder name = nixosConfigurations attribute (see flake.nix host +# auto-discovery), so this host is exposed as `nixosConfigurations.persistent-disk`. +# Unlike the live ISO (hosts/live), this builds a *persistent* disk image +# (qcow2 or raw) using disko's image builder: it carries the real on-disk GPT +# layout (1 GB ESP + ext4 root from nixos/disko-standard.nix) and state +# survives reboots, exactly like a machine you ran nixos/install.sh on. +# +# Build (the format is chosen at build time, see Makefile / README): +# +# make persistent-disk/qcow2 # qcow2 for this machine's arch +# make persistent-disk/raw # raw (dd-able straight to a drive) +# make persistent-disk/qcow2/aarch64-linux # cross-arch (needs a matching builder) +# +# # without make, e.g. a raw image: +# nix build .#nixosConfigurations.persistent-disk.config.system.build.diskoImages +# # (override disko.imageBuilder.imageFormat = "qcow2" for qcow2) +# +# This host is independent of nixos/install.sh; it shares the disk LAYOUT with +# real installs (disko-standard.nix) but is never itself part of the install +# flow. The turn-key login + Coder admin bootstrap (shared with the live ISO) +# live in nixos/box-turnkey.nix. + +{ lib, ... }: + +{ + imports = [ + ../../nixos/disko-standard.nix # 1 GB ESP + ext4 root single-disk layout + ../../nixos/box-turnkey.nix # shared turn-key config (login + Coder bootstrap) + ] ++ lib.optional (builtins.pathExists ./local.nix) ./local.nix; + + # disko writes the image for this device node; /dev/vda is the virtio disk a + # built image is partitioned against. The on-disk filesystems mount by LABEL + # (see disko-standard.nix), so the image still boots if the runtime device + # node differs (sda/nvme0n1/etc.). + disko.devices.disk.main.device = lib.mkForce "/dev/vda"; + + # The image is built offline in a VM with no EFI variable store, so install + # the bootloader without touching EFI variables. systemd-boot (enabled by + # default in configuration.nix) also writes the removable EFI fallback path + # (EFI/BOOT/BOOTX64.EFI), so the image still boots on firmware that has no + # pre-existing boot entry. + boot.loader.efi.canTouchEfiVariables = lib.mkForce false; +} diff --git a/nixos/box-turnkey.nix b/nixos/box-turnkey.nix new file mode 100644 index 0000000..2c7913e --- /dev/null +++ b/nixos/box-turnkey.nix @@ -0,0 +1,64 @@ +# Shared "turn-key" Box™ config — the bits that make an image boot straight +# into a fully-configured, ready-to-use Coder box with no install step. +# +# Imported by both image flavours: +# - nixos/live-iso.nix (live, ephemeral ISO: hosts/live) +# - hosts/persistent-disk/ (persistent disk image: qcow2 / raw) +# +# On real installs these settings come from nixos/install.sh + the gitignored +# hosts//local.nix it generates. The image flavours have no install step, +# so this module supplies the same turn-key defaults (same values the installer +# defaults to). Change them before handing an image to anyone untrusted, or +# override per-image via hosts//local.nix. + +{ config, lib, pkgs, modulesPath, self, inputs, ... }: + +{ + imports = [ + # Broad driver/firmware set so the image boots on arbitrary hardware / + # virtual machines. This replaces the per-host facter.json / + # hardware-configuration.nix that installed hosts rely on (image hosts + # ship neither). + (modulesPath + "/profiles/all-hardware.nix") + ]; + + # ── Bake the repo into the image at /etc/nixos-repo ────────────────────────── + # The on-disk installer copies the working tree to /etc/nixos-repo; the Coder + # bootstrap units (coder-init-admin.service, the coder-template-sync + # activation script) read templates from /etc/nixos-repo/coderd and the + # locally-packaged provider mirror. Point /etc/nixos-repo at the flake source + # baked into the image so it deploys templates exactly like an installed box. + # (The git-commit lookups in those scripts fall back to "unknown" when no + # .git is present, which is fine.) + environment.etc."nixos-repo".source = self.outPath; + + # Make the pinned nixpkgs resolvable on the box so `nix` / flake commands + # behave like an installed system, without shipping a channel. + nix.registry.nixpkgs.flake = inputs.nixpkgs; + + # ── Login + Coder admin bootstrap ──────────────────────────────────────────── + # Autologin drops straight into the Plasma desktop, mirroring a + # freshly-installed, configured box. + services.displayManager.autoLogin = { + enable = true; + user = "coderbox"; + }; + + users.users.coderbox = { + isNormalUser = true; + description = "coderbox"; + extraGroups = [ "networkmanager" "wheel" ]; + packages = [ pkgs.kdePackages.kate ]; + initialPassword = "coderbox"; + }; + + # coder-init-admin.service reads CODER_ADMIN_* from coder.service's + # environment and creates a local admin on first boot, then mints a session + # token and deploys the templates from /etc/nixos-repo/coderd. With these set + # the Coder instance is ready to use immediately. + systemd.services.coder.environment = { + CODER_ADMIN_EMAIL = "admin@coder.com"; + CODER_ADMIN_USERNAME = "admin"; + CODER_ADMIN_PASSWORD = "PleaseChangeMe1234"; + }; +} diff --git a/nixos/live-iso.nix b/nixos/live-iso.nix index 89f7348..798dbca 100644 --- a/nixos/live-iso.nix +++ b/nixos/live-iso.nix @@ -1,41 +1,38 @@ # Live "Box" ISO module — "it's just The Box™", not an installer. # -# Turns the shared Coder box configuration (configuration.nix) into a bootable -# live ISO that runs entirely from the USB/CD + RAM, with no disk install. When -# you boot this image you get the exact same system the on-disk install -# produces: KDE Plasma, the Coder server, k3s, rootless Podman, the bundled -# workspace templates, etc. — all started up automatically, just as if it were -# the disk you installed to. +# Turns the shared Coder box configuration into a bootable *ephemeral* live ISO +# that runs entirely from the USB/CD + RAM, with no disk install. Booting it +# gives the same system the on-disk install produces (KDE, Coder server, k3s, +# Podman, the bundled templates, all started automatically) — but the root +# filesystem is a squashfs + tmpfs overlay, so all state is discarded on +# reboot. For a *persistent* image (state survives reboots) build the +# persistent-disk host instead (qcow2 / raw); see the Makefile / README. # -# Build the ISO (folder name `live` => nixosConfigurations.live, see flake.nix): +# Build (folder name `live` => nixosConfigurations.live, see flake.nix): # -# nix build .#nixosConfigurations.live.config.system.build.isoImage +# make live-ephemeral-iso +# # or: nix build .#nixosConfigurations.live.config.system.build.isoImage # # → result/iso/coder-box-live-*.iso (flash with `dd`, Ventoy, etc.) # -# This module is imported only by hosts/live/default.nix and is completely -# independent of the regular disk-install flow (nixos/install.sh, disko, -# nixos-facter). It imports NO disko / hardware-configuration.nix / facter.json: -# the live root filesystem is the squashfs + tmpfs overlay that nixpkgs' -# iso-image.nix sets up, so there is no on-disk partition to format or mount. +# This module is imported only by hosts/live/default.nix and is independent of +# the regular disk-install flow (nixos/install.sh, disko, nixos-facter). It +# imports NO disko / hardware-configuration.nix / facter.json: the live root is +# the squashfs + tmpfs overlay that nixpkgs' iso-image.nix sets up. # -# Why the boot-loader overrides below: configuration.nix targets installed UEFI -# machines (systemd-boot + writing EFI variables). A live ISO is booted via the -# GRUB-EFI / isolinux loader baked into the image by iso-image.nix instead, so -# we force the installed-machine boot settings off to avoid eval conflicts and -# pointless EFI-variable writes on the host. +# The turn-key login + Coder admin bootstrap (shared with the persistent-disk +# image) live in nixos/box-turnkey.nix. -{ config, lib, pkgs, modulesPath, self, inputs, ... }: +{ config, lib, pkgs, modulesPath, ... }: { imports = [ # Core ISO builder: squashfs nix store, tmpfs overlay root, kernel/initrd, - # and the EFI + BIOS ISO bootloader. Provides the `system.build.isoImage` - # attribute and the `isoImage.*` options used below. + # and the EFI + BIOS ISO bootloader. Provides `system.build.isoImage` and + # the `isoImage.*` options used below. (modulesPath + "/installer/cd-dvd/iso-image.nix") - # Broad driver/firmware set so the ISO boots on arbitrary real hardware - # (this replaces the per-host facter.json / hardware-configuration.nix that - # installed hosts rely on). - (modulesPath + "/profiles/all-hardware.nix") + # Shared turn-key config (all-hardware, baked /etc/nixos-repo, autologin, + # Coder admin bootstrap). + ./box-turnkey.nix ]; # ── ISO image settings ────────────────────────────────────────────────────── @@ -47,9 +44,8 @@ isoImage.makeUsbBootable = true; # `dd` straight to a USB stick and boot isoImage.volumeID = "BOX_LIVE"; # Prefix of the generated file name (result/iso/--.iso). - # `image.baseName` is the post-25.05 replacement for `isoImage.isoBaseName`. - # iso-image.nix already sets it ("nixos--"), so override with - # mkForce to win over that definition. + # iso-image.nix already sets image.baseName ("nixos--"), so + # override with mkForce to win over that definition. image.baseName = lib.mkForce "coder-box-live"; # ── Boot loader: let iso-image.nix own it ──────────────────────────────────── @@ -58,47 +54,4 @@ # host's EFI variables when the live system activates. boot.loader.systemd-boot.enable = lib.mkForce false; boot.loader.efi.canTouchEfiVariables = lib.mkForce false; - - # ── Bake the repo into the image at /etc/nixos-repo ────────────────────────── - # The on-disk installer copies the working tree to /etc/nixos-repo; the Coder - # bootstrap units (coder-init-admin.service, the coder-template-sync - # activation script) read templates from /etc/nixos-repo/coderd and the - # locally-packaged provider mirror. Point /etc/nixos-repo at the flake source - # baked into the ISO so the live box deploys templates exactly like an - # installed box. (The git-commit lookups in those scripts fall back to - # "unknown" when no .git is present, which is fine.) - environment.etc."nixos-repo".source = self.outPath; - - # Make the pinned nixpkgs resolvable on the live box so `nix` / flake commands - # behave like an installed system, without shipping a channel. - nix.registry.nixpkgs.flake = inputs.nixpkgs; - - # ── Login + Coder admin bootstrap ──────────────────────────────────────────── - # On installed hosts these come from the gitignored hosts//local.nix - # that install.sh generates. The live image has no install step, so provide - # turn-key defaults here (same defaults the installer uses). Autologin drops - # straight into the Plasma desktop, mirroring a freshly-installed, configured - # box. Change these before handing the ISO to anyone untrusted. - services.displayManager.autoLogin = { - enable = true; - user = "coderbox"; - }; - - users.users.coderbox = { - isNormalUser = true; - description = "coderbox"; - extraGroups = [ "networkmanager" "wheel" ]; - packages = [ pkgs.kdePackages.kate ]; - initialPassword = "coderbox"; - }; - - # coder-init-admin.service reads CODER_ADMIN_* from coder.service's - # environment and creates a local admin on first boot, then mints a session - # token and deploys the templates from /etc/nixos-repo/coderd. With these set - # the live Coder instance is ready to use immediately. - systemd.services.coder.environment = { - CODER_ADMIN_EMAIL = "admin@coder.com"; - CODER_ADMIN_USERNAME = "admin"; - CODER_ADMIN_PASSWORD = "PleaseChangeMe1234"; - }; } From 7079c43c43ccec35d3c4cff7d3e0d669dea60ce9 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 13:25:50 +0000 Subject: [PATCH 03/19] refactor: unify image build targets under appliance/[/] Rename Makefile targets to a single 'appliance' namespace: live-ephemeral-iso -> appliance/iso persistent-disk/qcow2 -> appliance/qcow2 persistent-disk/raw -> appliance/raw Each keeps the optional / suffix (e.g. appliance/iso/aarch64-linux). The underlying hosts (live, persistent-disk) and modules are unchanged; this is target naming + docs only. Update README.md/agents.md. --- Makefile | 49 +++++++++++++++++++++++++++---------------------- README.md | 19 +++++++++++-------- agents.md | 4 ++-- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index bea19ec..d1b80d8 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,23 @@ -# Coder box — image build targets. +# Coder box — appliance image build targets. # -# Requires Nix with flakes enabled (nix-command + flakes). All image builds run -# on Linux only; cross-arch builds need a matching builder (native remote -# builder or binfmt/QEMU emulation). +# An "appliance" is the box prebuilt as a bootable image (no nixos/install.sh): +# it boots straight into the fully-configured Coder box. Three formats: # -# make live-ephemeral-iso # live ISO (tmpfs overlay; state wiped on reboot) -# make persistent-disk/qcow2 # persistent qcow2 disk image (state persists) -# make persistent-disk/raw # persistent raw disk image (dd-able to a drive) +# make appliance/iso # live ISO (tmpfs overlay; state wiped on reboot) +# make appliance/qcow2 # disk image (persistent; boots in QEMU/libvirt) +# make appliance/raw # disk image (persistent; dd-able to a drive) # -# Each target also takes an architecture suffix; short names are normalized to +# Each format also takes an architecture suffix; short names are normalized to # a *-linux triple (e.g. aarch64 -> aarch64-linux): # -# make live-ephemeral-iso/x86_64-linux -# make persistent-disk/qcow2/aarch64-linux -# make persistent-disk/raw/aarch64 +# make appliance/iso/x86_64-linux +# make appliance/qcow2/aarch64-linux +# make appliance/raw/aarch64 +# +# Requires Nix with flakes enabled (nix-command + flakes). All builds run on +# Linux only; cross-arch builds need a matching builder (native remote builder +# or binfmt/QEMU emulation). qcow2/raw additionally boot a QEMU VM during the +# build (disko image builder), so they want KVM to be fast. # # Outputs land in ./result (printed out-path). Flash a raw image or the ISO to # a drive with e.g. @@ -27,7 +31,8 @@ norm_arch = $(if $(filter %-linux,$(1)),$(1),$(1)-linux) # Single build helper used by every target. extendModules lets us override # nixpkgs.hostPlatform (per-arch) and the disko image format from one recipe, -# so adding a flavour is just a thin target below — no duplicated nix plumbing. +# so adding a format/arch is just a thin target below — no duplicated nix +# plumbing. # $(1) = host (nixosConfigurations.) # $(2) = system.build. (isoImage | diskoImages) # $(3) = extra module fields (nix attrset body, may be empty) @@ -37,22 +42,22 @@ define box_build 'let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { $(if $(4),nixpkgs.hostPlatform = "$(call norm_arch,$(4))"; ) $(3) } ]; }).config.system.build.$(2)' endef -.PHONY: live-ephemeral-iso persistent-disk/qcow2 persistent-disk/raw +.PHONY: appliance/iso appliance/qcow2 appliance/raw -# ── Live ephemeral ISO (hosts/live) ───────────────────────────────────────── -live-ephemeral-iso: +# ── appliance/iso — live ephemeral ISO (hosts/live) ────────────────────────── +appliance/iso: $(call box_build,live,isoImage,,) -live-ephemeral-iso/%: +appliance/iso/%: $(call box_build,live,isoImage,,$*) -# ── Persistent disk image (hosts/persistent-disk), qcow2 ───────────────────── -persistent-disk/qcow2: +# ── appliance/qcow2 — persistent disk image (hosts/persistent-disk) ────────── +appliance/qcow2: $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,) -persistent-disk/qcow2/%: +appliance/qcow2/%: $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,$*) -# ── Persistent disk image (hosts/persistent-disk), raw (dd-able) ───────────── -persistent-disk/raw: +# ── appliance/raw — persistent disk image, dd-able (hosts/persistent-disk) ──── +appliance/raw: $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,) -persistent-disk/raw/%: +appliance/raw/%: $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,$*) diff --git a/README.md b/README.md index b86e9f8..02a33dc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ NixOS configuration for Coder demo and workshop boxes. flake.nix # entry point: nixosConfigurations. per machine flake.lock # pinned nixpkgs / disko / nixos-facter-modules configuration.nix # shared NixOS config (all machines) -Makefile # image build targets: live-ephemeral-iso, persistent-disk/{qcow2,raw} +Makefile # appliance build targets: appliance/{iso,qcow2,raw}[/] local.nix.example # template copied to hosts//local.nix by install.sh .gitignore # ignores hosts/*/local.nix nixos/ @@ -122,20 +122,23 @@ image flavours build the *exact same* configured system — KDE Plasma, the Code server, k3s, Podman, the bundled templates — with admin bootstrap and template deploy happening on boot just like a real install. Neither is an installer. -| Flavour | Host | State | Build | +These prebuilt images are called **appliances** (the box, prebuilt — no +`install.sh`). Build them with `make appliance/`: + +| Format | Host | State | Build | |---|---|---|---| -| **Live ISO** (ephemeral) | `live` | tmpfs overlay — wiped on reboot | `make live-ephemeral-iso` | -| **Persistent disk** (qcow2) | `persistent-disk` | persists across reboots | `make persistent-disk/qcow2` | -| **Persistent disk** (raw) | `persistent-disk` | persists across reboots | `make persistent-disk/raw` | +| **iso** (live, ephemeral) | `live` | tmpfs overlay — wiped on reboot | `make appliance/iso` | +| **qcow2** (persistent disk) | `persistent-disk` | persists across reboots | `make appliance/qcow2` | +| **raw** (persistent disk) | `persistent-disk` | persists across reboots | `make appliance/raw` | All builds need a Linux machine with Nix + flakes. Every target also takes an architecture suffix (short names are normalized to `*-linux`); cross-arch builds need a matching builder (native remote builder or binfmt/QEMU): ```sh -make live-ephemeral-iso/aarch64-linux -make persistent-disk/qcow2/aarch64-linux -make persistent-disk/raw/x86_64 +make appliance/iso/aarch64-linux +make appliance/qcow2/aarch64-linux +make appliance/raw/x86_64 ``` The turn-key login + Coder admin bootstrap shared by both flavours live in diff --git a/agents.md b/agents.md index c0b73c8..1e39cdd 100644 --- a/agents.md +++ b/agents.md @@ -191,9 +191,9 @@ sudo k3s kubectl describe pod -n coder-workspaces coderd-provider.nix # terraform-provider-coderd derivation hosts/ live/ # `live` host: ephemeral live "Box" ISO; no disko/facter/hardware-config - # build: make live-ephemeral-iso (or .../) + # build: make appliance/iso (or appliance/iso/) persistent-disk/ # `persistent-disk` host: persistent qcow2/raw disk image (disko image builder) - # build: make persistent-disk/qcow2 | make persistent-disk/raw (or .../) + # build: make appliance/qcow2 | make appliance/raw (or .../) coder-thinkcentre/ # folder name = hostname; default.nix has a hardware-model header comment default.nix # host module: imports facter/legacy + local.nix + thinkcentre-only services hardware-configuration.nix # legacy fallback (used until facter.json exists) From da8c228c8a1e0bfc11fc1305e147cb50b58549ad Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 13:37:14 +0000 Subject: [PATCH 04/19] appliance/iso: name the ISO coder-box-appliance.iso Set image.baseName = coder-box-appliance (was coder-box-live) so the live appliance ISO is emitted as coder-box-appliance--.iso. Update the filename in the doc/comment examples too. --- README.md | 2 +- hosts/live/default.nix | 2 +- nixos/live-iso.nix | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 02a33dc..9b8d351 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ ISO carries its own GRUB-EFI + isolinux loader (BIOS boot is x86-only, so the aarch64 ISO is EFI-only). Flash it (it's isohybrid) and boot: ```sh -sudo dd if=result/iso/coder-box-live-*.iso of=/dev/sdX bs=4M status=progress oflag=sync +sudo dd if=result/iso/coder-box-appliance-*.iso of=/dev/sdX bs=4M status=progress oflag=sync ``` ### Persistent disk image (`persistent-disk`) diff --git a/hosts/live/default.nix b/hosts/live/default.nix index c7e0714..d84808c 100644 --- a/hosts/live/default.nix +++ b/hosts/live/default.nix @@ -5,7 +5,7 @@ # Build the bootable ISO with: # # nix build .#nixosConfigurations.live.config.system.build.isoImage -# # → result/iso/coder-box-live-*.iso +# # → result/iso/coder-box-appliance-*.iso # # Unlike the install hosts (coder-thinkcentre, qemu-arm64), this host does NOT # import nixos/disko-standard.nix, hardware-configuration.nix, or facter.json: diff --git a/nixos/live-iso.nix b/nixos/live-iso.nix index 798dbca..2cfceef 100644 --- a/nixos/live-iso.nix +++ b/nixos/live-iso.nix @@ -12,7 +12,7 @@ # # make live-ephemeral-iso # # or: nix build .#nixosConfigurations.live.config.system.build.isoImage -# # → result/iso/coder-box-live-*.iso (flash with `dd`, Ventoy, etc.) +# # → result/iso/coder-box-appliance-*.iso (flash with `dd`, Ventoy, etc.) # # This module is imported only by hosts/live/default.nix and is independent of # the regular disk-install flow (nixos/install.sh, disko, nixos-facter). It @@ -46,7 +46,7 @@ # Prefix of the generated file name (result/iso/--.iso). # iso-image.nix already sets image.baseName ("nixos--"), so # override with mkForce to win over that definition. - image.baseName = lib.mkForce "coder-box-live"; + image.baseName = lib.mkForce "coder-box-appliance"; # ── Boot loader: let iso-image.nix own it ──────────────────────────────────── # configuration.nix sets these for installed UEFI machines; force them off so From 078ac55a45dd6e4bdbc9c4bd213964be707baee7 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 13:38:55 +0000 Subject: [PATCH 05/19] fix(make): bare appliance/* targets build for the builder's native arch The live/persistent-disk hosts inherit configuration.nix's nixpkgs.hostPlatform = lib.mkOptionDefault "x86_64-linux", so a bare 'make appliance/iso' (no / suffix) always evaluated as x86_64 even on an aarch64 host. Pin nixpkgs.hostPlatform in the box_build helper to builtins.currentSystem when no arch is given (--impure already set), so the default tracks the builder's native architecture. Explicit / targets are unchanged. --- Makefile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d1b80d8..f1520c4 100644 --- a/Makefile +++ b/Makefile @@ -32,14 +32,19 @@ norm_arch = $(if $(filter %-linux,$(1)),$(1),$(1)-linux) # Single build helper used by every target. extendModules lets us override # nixpkgs.hostPlatform (per-arch) and the disko image format from one recipe, # so adding a format/arch is just a thin target below — no duplicated nix -# plumbing. +# plumbing. We ALWAYS pin nixpkgs.hostPlatform: when no arch is given we use +# `builtins.currentSystem` (the builder's native arch), otherwise the bare +# `appliance/` targets would inherit configuration.nix's +# `nixpkgs.hostPlatform = lib.mkOptionDefault "x86_64-linux"` and always build +# x86_64 even on an aarch64 host. `--impure` is what makes currentSystem +# available. # $(1) = host (nixosConfigurations.) # $(2) = system.build. (isoImage | diskoImages) # $(3) = extra module fields (nix attrset body, may be empty) # $(4) = arch token (empty = builder's native arch) define box_build $(NIX) build --impure --no-write-lock-file --print-out-paths --expr \ - 'let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { $(if $(4),nixpkgs.hostPlatform = "$(call norm_arch,$(4))"; ) $(3) } ]; }).config.system.build.$(2)' + 'let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { nixpkgs.hostPlatform = "$(if $(4),$(call norm_arch,$(4)),$${builtins.currentSystem})"; $(3) } ]; }).config.system.build.$(2)' endef .PHONY: appliance/iso appliance/qcow2 appliance/raw From 1ba7de10e8bdd063797be5436b97df3cb41d37f1 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 13:54:09 +0000 Subject: [PATCH 06/19] make: emit appliance images into ./out via --out-link (no copy) - box_build now passes --out-link out/ so each build plants a GC-root symlink under ./out (e.g. out/appliance-iso, out/appliance-raw-aarch64-linux) pointing straight at the store path. Native Nix, no copying; the image still lives in /nix/store (unavoidable) but is surfaced in the repo and won't be garbage-collected. ./out is gitignored. - Name the disko disk image coder-box-appliance.{raw,qcow2} (imageName) to match the ISO's image.baseName. - README: document the out/ paths and update the dd example. Verified: --out-link creates the symlink (selftest build); persistent-disk still evals with imageName override (resolves to coder-box-appliance). --- .gitignore | 1 + Makefile | 10 +++++++++- README.md | 9 ++++++++- hosts/persistent-disk/default.nix | 6 ++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 17b872a..d319ec6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ hosts/*/local.nix *.tfstate* .terraform/ *.bak +out/ diff --git a/Makefile b/Makefile index f1520c4..6fa8cf0 100644 --- a/Makefile +++ b/Makefile @@ -42,8 +42,16 @@ norm_arch = $(if $(filter %-linux,$(1)),$(1),$(1)-linux) # $(2) = system.build. (isoImage | diskoImages) # $(3) = extra module fields (nix attrset body, may be empty) # $(4) = arch token (empty = builder's native arch) +# The built image lives in /nix/store (always — that's how Nix works), but +# `--out-link` plants a GC-root symlink to it under ./out (named after the +# target, e.g. out/appliance-iso, out/appliance-raw-aarch64-linux). That's the +# native, non-copy way to surface the result in the repo: ./out/ points +# straight at the store path, and being a GC root it won't be garbage-collected. +# ./out is gitignored. define box_build - $(NIX) build --impure --no-write-lock-file --print-out-paths --expr \ + @mkdir -p out + $(NIX) build --impure --no-write-lock-file --print-out-paths \ + --out-link 'out/$(subst /,-,$@)' --expr \ 'let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { nixpkgs.hostPlatform = "$(if $(4),$(call norm_arch,$(4)),$${builtins.currentSystem})"; $(3) } ]; }).config.system.build.$(2)' endef diff --git a/README.md b/README.md index 9b8d351..18a8e91 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,13 @@ make appliance/qcow2/aarch64-linux make appliance/raw/x86_64 ``` +Each target drops a `--out-link` (GC-root symlink) in `./out/` named after the +target — e.g. `out/appliance-iso`, `out/appliance-raw-aarch64-linux` — pointing +straight at the built image in the Nix store (no copy; `./out` is gitignored). +The ISO is then at `out/appliance-iso/iso/coder-box-appliance-*.iso`, and a disk +image at `out/appliance-raw/coder-box-appliance.raw` (or +`out/appliance-qcow2/coder-box-appliance.qcow2`). + The turn-key login + Coder admin bootstrap shared by both flavours live in [`nixos/box-turnkey.nix`](nixos/box-turnkey.nix): autologin to the `coderbox` desktop, and admin `admin@coder.com` / `PleaseChangeMe1234`. Coder comes up at @@ -160,7 +167,7 @@ ISO carries its own GRUB-EFI + isolinux loader (BIOS boot is x86-only, so the aarch64 ISO is EFI-only). Flash it (it's isohybrid) and boot: ```sh -sudo dd if=result/iso/coder-box-appliance-*.iso of=/dev/sdX bs=4M status=progress oflag=sync +sudo dd if=out/appliance-iso/iso/coder-box-appliance-*.iso of=/dev/sdX bs=4M status=progress oflag=sync ``` ### Persistent disk image (`persistent-disk`) diff --git a/hosts/persistent-disk/default.nix b/hosts/persistent-disk/default.nix index 69b12f0..14ad700 100644 --- a/hosts/persistent-disk/default.nix +++ b/hosts/persistent-disk/default.nix @@ -36,6 +36,12 @@ # node differs (sda/nvme0n1/etc.). disko.devices.disk.main.device = lib.mkForce "/dev/vda"; + # Output file name: disko defaults imageName to the disk attr name ("main"), + # which would produce main.raw / main.qcow2. Name it after the appliance so + # the built image is coder-box-appliance.raw / .qcow2 (matches the ISO's + # image.baseName). + disko.devices.disk.main.imageName = lib.mkForce "coder-box-appliance"; + # The image is built offline in a VM with no EFI variable store, so install # the bootloader without touching EFI variables. systemd-boot (enabled by # default in configuration.nix) also writes the removable EFI fallback path From 3389584f0330b7cc33987cdae6d8129e215e27bf Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 14:19:54 +0000 Subject: [PATCH 07/19] appliance/iso: include arch in the ISO file name isoName derives from image.baseName; the previous bare "coder-box-appliance" dropped the arch upstream normally carries. Append ${hostPlatform.system} so the file is coder-box-appliance-.iso (e.g. -x86_64-linux / -aarch64-linux), making the arch visible and avoiding collisions between arches in ./out. --- nixos/live-iso.nix | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nixos/live-iso.nix b/nixos/live-iso.nix index 2cfceef..9f0d638 100644 --- a/nixos/live-iso.nix +++ b/nixos/live-iso.nix @@ -43,10 +43,12 @@ isoImage.makeBiosBootable = pkgs.stdenv.hostPlatform.isx86; isoImage.makeUsbBootable = true; # `dd` straight to a USB stick and boot isoImage.volumeID = "BOX_LIVE"; - # Prefix of the generated file name (result/iso/--.iso). - # iso-image.nix already sets image.baseName ("nixos--"), so - # override with mkForce to win over that definition. - image.baseName = lib.mkForce "coder-box-appliance"; + # ISO file name. iso-image.nix derives isoName from image.baseName as + # ".iso", and defaults baseName to "nixos--". We + # override baseName (mkForce, to win over that default) but keep the arch + # suffix so the file is e.g. coder-box-appliance-aarch64-linux.iso — the arch + # is visible in the name and x86_64/aarch64 ISOs don't collide in ./out. + image.baseName = lib.mkForce "coder-box-appliance-${pkgs.stdenv.hostPlatform.system}"; # ── Boot loader: let iso-image.nix own it ──────────────────────────────────── # configuration.nix sets these for installed UEFI machines; force them off so From dc91f64fc875ee2877a6deb7014792e26e605df5 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 14:23:55 +0000 Subject: [PATCH 08/19] appliance/{raw,qcow2}: include arch in the disk image file name Match the ISO: set disko imageName to coder-box-appliance- so disk images are coder-box-appliance-.{raw,qcow2} (e.g. -x86_64-linux / -aarch64-linux). Arch is visible and the two arches don't collide in ./out. Update README paths. --- README.md | 5 +++-- hosts/persistent-disk/default.nix | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 18a8e91..7998cb7 100644 --- a/README.md +++ b/README.md @@ -145,8 +145,9 @@ Each target drops a `--out-link` (GC-root symlink) in `./out/` named after the target — e.g. `out/appliance-iso`, `out/appliance-raw-aarch64-linux` — pointing straight at the built image in the Nix store (no copy; `./out` is gitignored). The ISO is then at `out/appliance-iso/iso/coder-box-appliance-*.iso`, and a disk -image at `out/appliance-raw/coder-box-appliance.raw` (or -`out/appliance-qcow2/coder-box-appliance.qcow2`). +image at `out/appliance-raw/coder-box-appliance-*.raw` (or +`out/appliance-qcow2/coder-box-appliance-*.qcow2`). All names carry the arch, +e.g. `coder-box-appliance-aarch64-linux.iso`. The turn-key login + Coder admin bootstrap shared by both flavours live in [`nixos/box-turnkey.nix`](nixos/box-turnkey.nix): autologin to the `coderbox` diff --git a/hosts/persistent-disk/default.nix b/hosts/persistent-disk/default.nix index 14ad700..5ba3b33 100644 --- a/hosts/persistent-disk/default.nix +++ b/hosts/persistent-disk/default.nix @@ -22,7 +22,7 @@ # flow. The turn-key login + Coder admin bootstrap (shared with the live ISO) # live in nixos/box-turnkey.nix. -{ lib, ... }: +{ lib, pkgs, ... }: { imports = [ @@ -37,10 +37,12 @@ disko.devices.disk.main.device = lib.mkForce "/dev/vda"; # Output file name: disko defaults imageName to the disk attr name ("main"), - # which would produce main.raw / main.qcow2. Name it after the appliance so - # the built image is coder-box-appliance.raw / .qcow2 (matches the ISO's - # image.baseName). - disko.devices.disk.main.imageName = lib.mkForce "coder-box-appliance"; + # which would produce main.raw / main.qcow2. Name it after the appliance and + # include the arch (like the ISO's image.baseName) so the built image is + # coder-box-appliance-.raw / .qcow2 — arch visible, and x86_64/aarch64 + # images don't collide in ./out. + disko.devices.disk.main.imageName = + lib.mkForce "coder-box-appliance-${pkgs.stdenv.hostPlatform.system}"; # The image is built offline in a VM with no EFI variable store, so install # the bootloader without touching EFI variables. systemd-boot (enabled by From f0989b24e354fa32d4057e759fed960314d806db Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 14:45:06 +0000 Subject: [PATCH 09/19] chore: gitignore appliance image artifacts (*.iso, *.qcow2, *.raw) --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index d319ec6..c2d78e8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,9 @@ hosts/*/local.nix *.tfstate* .terraform/ *.bak + +# Appliance image build outputs (Makefile --out-link target dir + artifacts) out/ +*.iso +*.qcow2 +*.raw From 20ef1f1b220758d22dc2bf5b37df62a4ad5547f1 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 14:50:29 +0000 Subject: [PATCH 10/19] appliance/iso: boot-menu label 'NixOS - Coder Box Appliance' Set isoImage.appendToMenuLabel = " - Coder Box Appliance" (replaces the default " Installer"), so both the BIOS/isolinux and EFI/grub boot entries read 'NixOS - Coder Box Appliance' instead of '... Installer'. --- nixos/live-iso.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nixos/live-iso.nix b/nixos/live-iso.nix index 9f0d638..ac63fba 100644 --- a/nixos/live-iso.nix +++ b/nixos/live-iso.nix @@ -43,6 +43,12 @@ isoImage.makeBiosBootable = pkgs.stdenv.hostPlatform.isx86; isoImage.makeUsbBootable = true; # `dd` straight to a USB stick and boot isoImage.volumeID = "BOX_LIVE"; + # Boot-menu label (both the BIOS/isolinux and EFI/grub entries). The label is + # " "; the default append is + # " Installer", which is misleading here since this is the live appliance, not + # the installer. Append " - Coder Box Appliance" -> "NixOS - Coder + # Box Appliance". Leading space is required (it's concatenated directly). + isoImage.appendToMenuLabel = " - Coder Box Appliance"; # ISO file name. iso-image.nix derives isoName from image.baseName as # ".iso", and defaults baseName to "nixos--". We # override baseName (mkForce, to win over that default) but keep the arch From b0131902cabda6ec871143bef1a635761b58fa91 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 14:57:17 +0000 Subject: [PATCH 11/19] refactor: rename appliance host dirs to hosts/_appliance_iso and hosts/_appliance-disk - git mv hosts/live -> hosts/_appliance_iso, hosts/persistent-disk -> hosts/_appliance-disk (flake auto-discovers them as nixosConfigurations ._appliance_iso / ._appliance-disk). - flake.nix derives networking.hostName from the folder name, but a leading underscore is an invalid hostname (must start alphanumeric), so each host now sets networking.hostName = mkForce "appliance-iso" / "appliance-disk". - Update Makefile host args and README/agents.md references. Build targets (appliance/iso, appliance/{qcow2,raw}[/]) are unchanged. Verified: all four nixosConfigurations eval; ISO + raw disk derivations build; existing hosts unaffected. --- Makefile | 18 ++++++++-------- README.md | 10 ++++----- agents.md | 6 +++--- .../default.nix | 21 ++++++++++++------- hosts/{live => _appliance_iso}/default.nix | 16 +++++++++----- 5 files changed, 41 insertions(+), 30 deletions(-) rename hosts/{persistent-disk => _appliance-disk}/default.nix (73%) rename hosts/{live => _appliance_iso}/default.nix (53%) diff --git a/Makefile b/Makefile index 6fa8cf0..d93e105 100644 --- a/Makefile +++ b/Makefile @@ -57,20 +57,20 @@ endef .PHONY: appliance/iso appliance/qcow2 appliance/raw -# ── appliance/iso — live ephemeral ISO (hosts/live) ────────────────────────── +# ── appliance/iso — live ephemeral ISO (hosts/_appliance_iso) ──────────────── appliance/iso: - $(call box_build,live,isoImage,,) + $(call box_build,_appliance_iso,isoImage,,) appliance/iso/%: - $(call box_build,live,isoImage,,$*) + $(call box_build,_appliance_iso,isoImage,,$*) -# ── appliance/qcow2 — persistent disk image (hosts/persistent-disk) ────────── +# ── appliance/qcow2 — persistent disk image (hosts/_appliance-disk) ────────── appliance/qcow2: - $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,) + $(call box_build,_appliance-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,) appliance/qcow2/%: - $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,$*) + $(call box_build,_appliance-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,$*) -# ── appliance/raw — persistent disk image, dd-able (hosts/persistent-disk) ──── +# ── appliance/raw — persistent disk image, dd-able (hosts/_appliance-disk) ──── appliance/raw: - $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,) + $(call box_build,_appliance-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,) appliance/raw/%: - $(call box_build,persistent-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,$*) + $(call box_build,_appliance-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,$*) diff --git a/README.md b/README.md index 7998cb7..db6539a 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ nixos/ k3s-podman.nix # k3s + rootless Podman socket screenconnect.nix # optional ScreenConnect remote access client box-turnkey.nix # shared turn-key bits for prebuilt images (login + Coder bootstrap) - live-iso.nix # ephemeral live ISO module (hosts/live) + live-iso.nix # ephemeral live ISO module (hosts/_appliance_iso) pkgs/ coder.nix # custom Coder server package coderd-provider.nix # terraform-provider-coderd package @@ -52,9 +52,9 @@ hosts/ local.nix # gitignored: admin creds, secrets, SSH users templates/ nook-android/ # Workspace: build trmnl-nook-simple-touch APK - live/ # `live` host: ephemeral live "Box" ISO (no disk install) + _appliance_iso/ # `_appliance_iso` host: ephemeral live "Box" ISO (no disk install) default.nix # imports nixos/live-iso.nix only (no disko/facter/hardware-config) - persistent-disk/ # `persistent-disk` host: persistent qcow2/raw disk image + _appliance-disk/ # `_appliance-disk` host: persistent qcow2/raw disk image default.nix # imports disko-standard.nix + box-turnkey.nix coderd/ main.tf # manages all Coder templates via coderd Terraform provider @@ -160,7 +160,7 @@ desktop, and admin `admin@coder.com` / `PleaseChangeMe1234`. Coder comes up at The live root filesystem is the squashfs + tmpfs overlay from nixpkgs' `iso-image.nix`, so there's no partition to format or mount and **all state is -discarded on reboot**. `hosts/live/default.nix` imports +discarded on reboot**. `hosts/_appliance_iso/default.nix` imports [`nixos/live-iso.nix`](nixos/live-iso.nix) (which pulls in `box-turnkey.nix`) — **no** `disko-standard.nix`, `hardware-configuration.nix`, or `facter.json`. The installed-machine `systemd-boot` / EFI-variable settings are forced off; the @@ -176,7 +176,7 @@ sudo dd if=out/appliance-iso/iso/coder-box-appliance-*.iso of=/dev/sdX bs=4M sta Built with [disko](https://github.com/nix-community/disko)'s image builder, so it carries the real on-disk GPT layout from `nixos/disko-standard.nix` (1 GB ESP + ext4 root) and **state survives reboots**, exactly like a machine you ran -`install.sh` on. `hosts/persistent-disk/default.nix` imports +`install.sh` on. `hosts/_appliance-disk/default.nix` imports `disko-standard.nix` + `box-turnkey.nix`. - **`qcow2`** — boot it directly in QEMU/libvirt/UTM. A qcow2 is a container diff --git a/agents.md b/agents.md index 1e39cdd..c4823d7 100644 --- a/agents.md +++ b/agents.md @@ -185,14 +185,14 @@ sudo k3s kubectl describe pod -n coder-workspaces k3s-podman.nix # k3s + rootless Podman socket screenconnect.nix # ScreenConnect remote access client box-turnkey.nix # shared turn-key bits for prebuilt images (login + Coder bootstrap) - live-iso.nix # ephemeral live "Box" ISO module (imported by hosts/live) + live-iso.nix # ephemeral live "Box" ISO module (imported by hosts/_appliance_iso) pkgs/ coder.nix # Coder server package derivation coderd-provider.nix # terraform-provider-coderd derivation hosts/ - live/ # `live` host: ephemeral live "Box" ISO; no disko/facter/hardware-config + _appliance_iso/ # `_appliance_iso` host: ephemeral live "Box" ISO; no disko/facter/hardware-config # build: make appliance/iso (or appliance/iso/) - persistent-disk/ # `persistent-disk` host: persistent qcow2/raw disk image (disko image builder) + _appliance-disk/ # `_appliance-disk` host: persistent qcow2/raw disk image (disko image builder) # build: make appliance/qcow2 | make appliance/raw (or .../) coder-thinkcentre/ # folder name = hostname; default.nix has a hardware-model header comment default.nix # host module: imports facter/legacy + local.nix + thinkcentre-only services diff --git a/hosts/persistent-disk/default.nix b/hosts/_appliance-disk/default.nix similarity index 73% rename from hosts/persistent-disk/default.nix rename to hosts/_appliance-disk/default.nix index 5ba3b33..93e2aa6 100644 --- a/hosts/persistent-disk/default.nix +++ b/hosts/_appliance-disk/default.nix @@ -1,20 +1,20 @@ # Persistent "Box" disk image host — "it's just The Box™" on a real disk. # # Folder name = nixosConfigurations attribute (see flake.nix host -# auto-discovery), so this host is exposed as `nixosConfigurations.persistent-disk`. -# Unlike the live ISO (hosts/live), this builds a *persistent* disk image -# (qcow2 or raw) using disko's image builder: it carries the real on-disk GPT -# layout (1 GB ESP + ext4 root from nixos/disko-standard.nix) and state +# auto-discovery), so this host is exposed as `nixosConfigurations._appliance-disk`. +# Unlike the live ISO (hosts/_appliance_iso), this builds a *persistent* disk +# image (qcow2 or raw) using disko's image builder: it carries the real on-disk +# GPT layout (1 GB ESP + ext4 root from nixos/disko-standard.nix) and state # survives reboots, exactly like a machine you ran nixos/install.sh on. # # Build (the format is chosen at build time, see Makefile / README): # -# make persistent-disk/qcow2 # qcow2 for this machine's arch -# make persistent-disk/raw # raw (dd-able straight to a drive) -# make persistent-disk/qcow2/aarch64-linux # cross-arch (needs a matching builder) +# make appliance/qcow2 # qcow2 for this machine's arch +# make appliance/raw # raw (dd-able straight to a drive) +# make appliance/qcow2/aarch64-linux # cross-arch (needs a matching builder) # # # without make, e.g. a raw image: -# nix build .#nixosConfigurations.persistent-disk.config.system.build.diskoImages +# nix build .#nixosConfigurations._appliance-disk.config.system.build.diskoImages # # (override disko.imageBuilder.imageFormat = "qcow2" for qcow2) # # This host is independent of nixos/install.sh; it shares the disk LAYOUT with @@ -30,6 +30,11 @@ ../../nixos/box-turnkey.nix # shared turn-key config (login + Coder bootstrap) ] ++ lib.optional (builtins.pathExists ./local.nix) ./local.nix; + # flake.nix defaults networking.hostName to the folder name, but + # "_appliance-disk" is not a valid hostname (must start with an alphanumeric). + # Use a valid appliance hostname. + networking.hostName = lib.mkForce "appliance-disk"; + # disko writes the image for this device node; /dev/vda is the virtio disk a # built image is partitioned against. The on-disk filesystems mount by LABEL # (see disko-standard.nix), so the image still boots if the runtime device diff --git a/hosts/live/default.nix b/hosts/_appliance_iso/default.nix similarity index 53% rename from hosts/live/default.nix rename to hosts/_appliance_iso/default.nix index d84808c..d14a874 100644 --- a/hosts/live/default.nix +++ b/hosts/_appliance_iso/default.nix @@ -1,11 +1,12 @@ -# Live "Box" ISO host — "it's just The Box™", not an installer. +# Live "Box" ISO appliance host — "it's just The Box™", not an installer. # # Folder name = nixosConfigurations attribute (see flake.nix host -# auto-discovery), so this host is exposed as `nixosConfigurations.live`. -# Build the bootable ISO with: +# auto-discovery), so this host is exposed as `nixosConfigurations._appliance_iso`. +# It's normally built via the Makefile rather than by attribute: # -# nix build .#nixosConfigurations.live.config.system.build.isoImage -# # → result/iso/coder-box-appliance-*.iso +# make appliance/iso # → out/appliance-iso/iso/coder-box-appliance-*.iso +# # equivalently: +# nix build .#nixosConfigurations._appliance_iso.config.system.build.isoImage # # Unlike the install hosts (coder-thinkcentre, qemu-arm64), this host does NOT # import nixos/disko-standard.nix, hardware-configuration.nix, or facter.json: @@ -20,4 +21,9 @@ { imports = [ ../../nixos/live-iso.nix ] ++ lib.optional (builtins.pathExists ./local.nix) ./local.nix; + + # flake.nix defaults networking.hostName to the folder name, but + # "_appliance_iso" is not a valid hostname (must start with an alphanumeric; + # underscores are only allowed mid-name). Use a valid appliance hostname. + networking.hostName = lib.mkForce "appliance-iso"; } From 3140f83237466ae1774246577f318bf248bdeb37 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 15:08:13 +0000 Subject: [PATCH 12/19] feat: default hostname to coder-box centrally; appliances inherit it - configuration.nix sets networking.hostName = mkOverride 1250 "coder-box" as the central default. Priority 1250 sits between mkDefault (1000) and mkOptionDefault (1500): it overrides nixpkgs' own mkOptionDefault "nixos" (which would otherwise tie and error) but still loses to flake.nix's folder-name mkDefault on install hosts. - flake.nix injects the folder-name hostname only for non-underscore hosts; underscore-prefixed image hosts (_appliance_iso, _appliance-disk) skip it and inherit coder-box. - Remove the custom hostName overrides from the appliance hosts (no longer set in the appliance nix files). - coder-thinkcentre and qemu-arm64 keep their folder-name hostnames. - Docs updated. Verified: _appliance_iso/_appliance-disk -> coder-box; coder-thinkcentre -> coder-thinkcentre; qemu-arm64 -> qemu-arm64; thinkcentre toplevel drv hash unchanged; ISO + raw disk drvs build. --- README.md | 13 +++++++++---- configuration.nix | 16 +++++++++++++--- flake.nix | 23 +++++++++++++++++------ hosts/_appliance-disk/default.nix | 7 +++---- hosts/_appliance_iso/default.nix | 7 +++---- 5 files changed, 45 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index db6539a..73a1ee9 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,15 @@ coderd/ This repo is a Nix flake. `flake.nix` auto-discovers every subdirectory of `./hosts/` that contains a `default.nix` and exposes it as -`nixosConfigurations.`. The folder name is the hostname, so -`nixos-rebuild switch --flake .` auto-selects the right config on the -running box. Adding a new host means creating a host folder, no flake.nix -edit. The installer does this for you. +`nixosConfigurations.`. For normal install hosts the folder name +is also the hostname, so `nixos-rebuild switch --flake .` auto-selects the +right config on the running box. Adding a new host means creating a host +folder, no flake.nix edit. The installer does this for you. + +Hosts whose folder name starts with an underscore (`_appliance_iso`, +`_appliance-disk`) are image/appliance builds, not per-machine installs: they +do **not** get the folder-name hostname and instead inherit the central +default `networking.hostName = "coder-box"` (set in `configuration.nix`). Two community tools do the heavy lifting: diff --git a/configuration.nix b/configuration.nix index 1c88c8f..983e839 100644 --- a/configuration.nix +++ b/configuration.nix @@ -106,9 +106,19 @@ in zramSwap.enable = lib.mkDefault true; # ── Networking ──────────────────────────────────────────────────────────── - # networking.hostName is set by flake.nix's mkHost to the host folder - # name; per-host modules can override with lib.mkForce in - # hosts//local.nix or default.nix. + # Central default hostname. Install hosts override this: flake.nix's mkHost + # injects `networking.hostName = lib.mkDefault ` for every + # non-underscore host (so coder-thinkcentre stays coder-thinkcentre, etc.). + # Underscore-prefixed image/appliance hosts (_appliance_iso, _appliance-disk) + # get no injection and so inherit "coder-box". + # + # Priority 1250 (mkOverride) is deliberately BETWEEN mkDefault (1000) and + # mkOptionDefault (1500): it beats the option's own built-in default + # ("nixos", which nixpkgs sets at mkOptionDefault and would otherwise tie + # and error), while still losing to flake.nix's mkDefault folder-name + # injection on install hosts. A host's local.nix/default.nix can override at + # normal (100) priority or mkForce. + networking.hostName = lib.mkOverride 1250 "coder-box"; networking.networkmanager.enable = true; # mDNS: every box reachable as .local on the LAN diff --git a/flake.nix b/flake.nix index 9685672..32416ea 100644 --- a/flake.nix +++ b/flake.nix @@ -37,10 +37,12 @@ forAllSystems = lib.genAttrs systems; # Each subdirectory of ./hosts that contains a default.nix becomes a - # nixosConfigurations entry. The folder name IS the hostname, so - # `nixos-rebuild switch --flake .` auto-selects the right config on - # the running box without needing `.#`. Adding a new host means - # just creating ./hosts//default.nix; no flake.nix edit. + # nixosConfigurations entry. For install hosts the folder name IS the + # hostname, so `nixos-rebuild switch --flake .` auto-selects the right + # config on the running box without needing `.#`. Adding a new host + # means just creating ./hosts//default.nix; no flake.nix edit. + # (Underscore-prefixed folders like _appliance_iso are image builds that + # skip the folder-name hostname; see mkHost below.) hostNames = lib.attrNames (lib.filterAttrs (name: type: type == "directory" @@ -58,8 +60,17 @@ disko.nixosModules.disko nixos-facter-modules.nixosModules.facter (./hosts + "/${hostname}") - { networking.hostName = lib.mkDefault hostname; } - ]; + ] + # Install hosts use their folder name as the hostname so + # `nixos-rebuild switch --flake .` auto-selects the right config on the + # running box. Underscore-prefixed folders (e.g. _appliance_iso, + # _appliance-disk) are image/appliance builds whose names aren't valid + # hostnames and aren't installed per-machine; they fall through to the + # central default (networking.hostName = "coder-box" in + # configuration.nix). mkDefault here (1000) overrides that central + # mkOptionDefault (1500) for install hosts. + ++ lib.optional (!lib.hasPrefix "_" hostname) + { networking.hostName = lib.mkDefault hostname; }; }; in { nixosConfigurations = diff --git a/hosts/_appliance-disk/default.nix b/hosts/_appliance-disk/default.nix index 93e2aa6..b61656c 100644 --- a/hosts/_appliance-disk/default.nix +++ b/hosts/_appliance-disk/default.nix @@ -30,10 +30,9 @@ ../../nixos/box-turnkey.nix # shared turn-key config (login + Coder bootstrap) ] ++ lib.optional (builtins.pathExists ./local.nix) ./local.nix; - # flake.nix defaults networking.hostName to the folder name, but - # "_appliance-disk" is not a valid hostname (must start with an alphanumeric). - # Use a valid appliance hostname. - networking.hostName = lib.mkForce "appliance-disk"; + # No networking.hostName here on purpose: underscore-prefixed image hosts get + # no folder-name injection from flake.nix and inherit the central default + # "coder-box" (configuration.nix). Override in local.nix if you need another. # disko writes the image for this device node; /dev/vda is the virtio disk a # built image is partitioned against. The on-disk filesystems mount by LABEL diff --git a/hosts/_appliance_iso/default.nix b/hosts/_appliance_iso/default.nix index d14a874..5abe6bc 100644 --- a/hosts/_appliance_iso/default.nix +++ b/hosts/_appliance_iso/default.nix @@ -22,8 +22,7 @@ imports = [ ../../nixos/live-iso.nix ] ++ lib.optional (builtins.pathExists ./local.nix) ./local.nix; - # flake.nix defaults networking.hostName to the folder name, but - # "_appliance_iso" is not a valid hostname (must start with an alphanumeric; - # underscores are only allowed mid-name). Use a valid appliance hostname. - networking.hostName = lib.mkForce "appliance-iso"; + # No networking.hostName here on purpose: underscore-prefixed image hosts get + # no folder-name injection from flake.nix and inherit the central default + # "coder-box" (configuration.nix). Override in local.nix if you need another. } From 4983902891e6629aee0e55309b1390f0b0a4ac52 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Fri, 5 Jun 2026 19:44:33 +0000 Subject: [PATCH 13/19] fix: stop appliance ISO growing every build (filter build artifacts from /etc/nixos-repo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: environment.etc."nixos-repo".source = self.outPath bakes the flake source into the image. On a DIRTY working tree, nix copies untracked files into self.outPath even when gitignored — including the Makefile's ./out (built images land there) and stray *.iso/*.qcow2/*.raw. So each build embedded the previous image into /etc/nixos-repo -> squashfs -> the next image, growing the ISO on every rebuild (verified: a 300MB out/ file inflated self.outPath to 206MB). Fix: wrap the baked source in lib.cleanSourceWith, filtering out out/, result, result-*, and *.iso/*.qcow2/*.raw. The baked /etc/nixos-repo is now content- stable regardless of build artifacts (verified: source hash unchanged when out/ grows 300MB->600MB), while still shipping the full tree (coderd/ etc.) for nixos-rebuild / coder-reset. Verified: baked source 5.5MB (no out/, no stray.iso, coderd/ kept); ISO + raw disk drvs evaluate; coder-thinkcentre toplevel drv hash unchanged. --- nixos/box-turnkey.nix | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/nixos/box-turnkey.nix b/nixos/box-turnkey.nix index 2c7913e..87cb17e 100644 --- a/nixos/box-turnkey.nix +++ b/nixos/box-turnkey.nix @@ -30,7 +30,28 @@ # baked into the image so it deploys templates exactly like an installed box. # (The git-commit lookups in those scripts fall back to "unknown" when no # .git is present, which is fine.) - environment.etc."nixos-repo".source = self.outPath; + # + # IMPORTANT: filter out build artifacts before baking. `self.outPath` is the + # flake source, but on a DIRTY working tree `getFlake`/`nix build .#…` copies + # untracked files into it *even if they're gitignored* — including the + # Makefile's ./out (where built images land) and any stray *.iso/*.qcow2/*.raw + # in the repo. Baking that unfiltered means each build's image gets embedded + # into /etc/nixos-repo → into the squashfs → into the *next* image, so the ISO + # grows on every rebuild (a feedback loop). cleanSourceWith strips those paths + # so the baked repo is stable regardless of build artifacts, while still + # shipping the full tree (coderd/ etc.) for nixos-rebuild / coder-reset. + environment.etc."nixos-repo".source = lib.cleanSourceWith { + name = "nixos-repo-src"; + src = self.outPath; + filter = path: type: + let base = baseNameOf (toString path); in + base != "out" + && base != "result" + && !(lib.hasPrefix "result-" base) + && !(lib.hasSuffix ".iso" base) + && !(lib.hasSuffix ".qcow2" base) + && !(lib.hasSuffix ".raw" base); + }; # Make the pinned nixpkgs resolvable on the box so `nix` / flake commands # behave like an installed system, without shipping a channel. From 8e4047f6594a6c9ce0fdcd91bcc24a0bf3656dae Mon Sep 17 00:00:00 2001 From: Phorcys Date: Mon, 8 Jun 2026 10:07:17 +0000 Subject: [PATCH 14/19] docs: mark qcow2/raw disk appliances as untested; fix stale host names - Add an 'untested' warning + Status column for the qcow2/raw disk images; only appliance/iso is verified to build and boot so far. - Fix lingering host names in the prebuilt-images section (live -> _appliance_iso, persistent-disk -> _appliance-disk). --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 73a1ee9..01e91df 100644 --- a/README.md +++ b/README.md @@ -130,11 +130,11 @@ deploy happening on boot just like a real install. Neither is an installer. These prebuilt images are called **appliances** (the box, prebuilt — no `install.sh`). Build them with `make appliance/`: -| Format | Host | State | Build | -|---|---|---|---| -| **iso** (live, ephemeral) | `live` | tmpfs overlay — wiped on reboot | `make appliance/iso` | -| **qcow2** (persistent disk) | `persistent-disk` | persists across reboots | `make appliance/qcow2` | -| **raw** (persistent disk) | `persistent-disk` | persists across reboots | `make appliance/raw` | +| Format | Host | State | Status | Build | +|---|---|---|---|---| +| **iso** (live, ephemeral) | `_appliance_iso` | tmpfs overlay — wiped on reboot | verified | `make appliance/iso` | +| **qcow2** (persistent disk) | `_appliance-disk` | persists across reboots | ⚠️ untested | `make appliance/qcow2` | +| **raw** (persistent disk) | `_appliance-disk` | persists across reboots | ⚠️ untested | `make appliance/raw` | All builds need a Linux machine with Nix + flakes. Every target also takes an architecture suffix (short names are normalized to `*-linux`); cross-arch @@ -176,7 +176,14 @@ aarch64 ISO is EFI-only). Flash it (it's isohybrid) and boot: sudo dd if=out/appliance-iso/iso/coder-box-appliance-*.iso of=/dev/sdX bs=4M status=progress oflag=sync ``` -### Persistent disk image (`persistent-disk`) +### Persistent disk image (`_appliance-disk`) + +> [!WARNING] +> **Untested.** The `qcow2` and `raw` disk-image builds evaluate cleanly and +> produce a valid build plan, but they have not yet been built end-to-end or +> boot-tested. The live `appliance/iso` is the only flavour verified to build +> and boot so far. Treat the disk images as experimental until someone confirms +> a working build + boot. Built with [disko](https://github.com/nix-community/disko)'s image builder, so it carries the real on-disk GPT layout from `nixos/disko-standard.nix` (1 GB From 9b0b1ded35486b31d5618694642aeef65f5de356 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Mon, 8 Jun 2026 10:07:32 +0000 Subject: [PATCH 15/19] docs: finish renaming stale live/persistent-disk host refs in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 01e91df..49ced02 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ desktop, and admin `admin@coder.com` / `PleaseChangeMe1234`. Coder comes up at `/etc/motd`). Change these before sharing an image by dropping a gitignored `hosts//local.nix` (same shape as `local.nix.example`). -### Live ISO (`live`) +### Live ISO (`_appliance_iso`) The live root filesystem is the squashfs + tmpfs overlay from nixpkgs' `iso-image.nix`, so there's no partition to format or mount and **all state is @@ -201,7 +201,7 @@ ESP + ext4 root) and **state survives reboots**, exactly like a machine you ran Both image hosts are completely separate from the disk-install flow above (`nixos/install.sh`, `nixos-facter`); adding them changes nothing for normal -installs. The `persistent-disk` host shares only the disk *layout* +installs. The `_appliance-disk` host shares only the disk *layout* (`disko-standard.nix`) with real installs, never the install process itself. ## After install From b84b169c0b46ba7b0d0cd4b0518233af9b854e22 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Mon, 8 Jun 2026 10:22:00 +0000 Subject: [PATCH 16/19] refactor: move appliance modules under nixos/_appliance/; 'Live ISO' -> 'Appliance ISO' - git mv nixos/box-turnkey.nix -> nixos/_appliance/box-turnkey.nix - git mv nixos/live-iso.nix -> nixos/_appliance/live-iso.nix - Update import paths in hosts/_appliance_iso and hosts/_appliance-disk to ../../nixos/_appliance/...; live-iso.nix's ./box-turnkey.nix import is unchanged (both files moved together). - Reword 'Live ISO' -> 'Appliance ISO' (and 'live root' -> 'appliance root') in module headers, host comments, Makefile, README, and agents.md. - Update repo-structure listings to show the nixos/_appliance/ subfolder. Verified: all four nixosConfigurations eval; ISO + raw disk derivations build with the new import paths. --- Makefile | 4 ++-- README.md | 19 ++++++++------- agents.md | 5 ++-- hosts/_appliance-disk/default.nix | 8 +++---- hosts/_appliance_iso/default.nix | 6 ++--- nixos/{ => _appliance}/box-turnkey.nix | 6 ++--- nixos/{ => _appliance}/live-iso.nix | 33 +++++++++++++------------- 7 files changed, 42 insertions(+), 39 deletions(-) rename nixos/{ => _appliance}/box-turnkey.nix (95%) rename nixos/{ => _appliance}/live-iso.nix (71%) diff --git a/Makefile b/Makefile index d93e105..9c0a754 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # An "appliance" is the box prebuilt as a bootable image (no nixos/install.sh): # it boots straight into the fully-configured Coder box. Three formats: # -# make appliance/iso # live ISO (tmpfs overlay; state wiped on reboot) +# make appliance/iso # appliance ISO (tmpfs overlay; state wiped on reboot) # make appliance/qcow2 # disk image (persistent; boots in QEMU/libvirt) # make appliance/raw # disk image (persistent; dd-able to a drive) # @@ -57,7 +57,7 @@ endef .PHONY: appliance/iso appliance/qcow2 appliance/raw -# ── appliance/iso — live ephemeral ISO (hosts/_appliance_iso) ──────────────── +# ── appliance/iso — ephemeral appliance ISO (hosts/_appliance_iso) ─────────── appliance/iso: $(call box_build,_appliance_iso,isoImage,,) appliance/iso/%: diff --git a/README.md b/README.md index 49ced02..6463ca6 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,9 @@ nixos/ k3s-sysbox.nix # k3s + sysbox-runc runtime class k3s-podman.nix # k3s + rootless Podman socket screenconnect.nix # optional ScreenConnect remote access client - box-turnkey.nix # shared turn-key bits for prebuilt images (login + Coder bootstrap) - live-iso.nix # ephemeral live ISO module (hosts/_appliance_iso) + _appliance/ # prebuilt-appliance modules (ISO + persistent disk) + box-turnkey.nix # shared turn-key bits for appliances (login + Coder bootstrap) + live-iso.nix # ephemeral appliance ISO module (hosts/_appliance_iso) pkgs/ coder.nix # custom Coder server package coderd-provider.nix # terraform-provider-coderd package @@ -52,10 +53,10 @@ hosts/ local.nix # gitignored: admin creds, secrets, SSH users templates/ nook-android/ # Workspace: build trmnl-nook-simple-touch APK - _appliance_iso/ # `_appliance_iso` host: ephemeral live "Box" ISO (no disk install) - default.nix # imports nixos/live-iso.nix only (no disko/facter/hardware-config) + _appliance_iso/ # `_appliance_iso` host: ephemeral appliance ISO (no disk install) + default.nix # imports nixos/_appliance/live-iso.nix (no disko/facter/hardware-config) _appliance-disk/ # `_appliance-disk` host: persistent qcow2/raw disk image - default.nix # imports disko-standard.nix + box-turnkey.nix + default.nix # imports disko-standard.nix + nixos/_appliance/box-turnkey.nix coderd/ main.tf # manages all Coder templates via coderd Terraform provider templates/ @@ -155,18 +156,18 @@ image at `out/appliance-raw/coder-box-appliance-*.raw` (or e.g. `coder-box-appliance-aarch64-linux.iso`. The turn-key login + Coder admin bootstrap shared by both flavours live in -[`nixos/box-turnkey.nix`](nixos/box-turnkey.nix): autologin to the `coderbox` +[`nixos/_appliance/box-turnkey.nix`](nixos/_appliance/box-turnkey.nix): autologin to the `coderbox` desktop, and admin `admin@coder.com` / `PleaseChangeMe1234`. Coder comes up at `http://.local:3000` (or the `*.try.coder.app` tunnel URL in `/etc/motd`). Change these before sharing an image by dropping a gitignored `hosts//local.nix` (same shape as `local.nix.example`). -### Live ISO (`_appliance_iso`) +### Appliance ISO (`_appliance_iso`) -The live root filesystem is the squashfs + tmpfs overlay from nixpkgs' +The appliance root filesystem is the squashfs + tmpfs overlay from nixpkgs' `iso-image.nix`, so there's no partition to format or mount and **all state is discarded on reboot**. `hosts/_appliance_iso/default.nix` imports -[`nixos/live-iso.nix`](nixos/live-iso.nix) (which pulls in `box-turnkey.nix`) — +[`nixos/_appliance/live-iso.nix`](nixos/_appliance/live-iso.nix) (which pulls in `box-turnkey.nix`) — **no** `disko-standard.nix`, `hardware-configuration.nix`, or `facter.json`. The installed-machine `systemd-boot` / EFI-variable settings are forced off; the ISO carries its own GRUB-EFI + isolinux loader (BIOS boot is x86-only, so the diff --git a/agents.md b/agents.md index c4823d7..deadcc0 100644 --- a/agents.md +++ b/agents.md @@ -184,8 +184,9 @@ sudo k3s kubectl describe pod -n coder-workspaces k3s-sysbox.nix # k3s + sysbox runtime k3s-podman.nix # k3s + rootless Podman socket screenconnect.nix # ScreenConnect remote access client - box-turnkey.nix # shared turn-key bits for prebuilt images (login + Coder bootstrap) - live-iso.nix # ephemeral live "Box" ISO module (imported by hosts/_appliance_iso) + _appliance/ # prebuilt-appliance modules (ISO + persistent disk) + box-turnkey.nix # shared turn-key bits for appliances (login + Coder bootstrap) + live-iso.nix # ephemeral appliance ISO module (imported by hosts/_appliance_iso) pkgs/ coder.nix # Coder server package derivation coderd-provider.nix # terraform-provider-coderd derivation diff --git a/hosts/_appliance-disk/default.nix b/hosts/_appliance-disk/default.nix index b61656c..c90a3e0 100644 --- a/hosts/_appliance-disk/default.nix +++ b/hosts/_appliance-disk/default.nix @@ -2,7 +2,7 @@ # # Folder name = nixosConfigurations attribute (see flake.nix host # auto-discovery), so this host is exposed as `nixosConfigurations._appliance-disk`. -# Unlike the live ISO (hosts/_appliance_iso), this builds a *persistent* disk +# Unlike the appliance ISO (hosts/_appliance_iso), this builds a *persistent* disk # image (qcow2 or raw) using disko's image builder: it carries the real on-disk # GPT layout (1 GB ESP + ext4 root from nixos/disko-standard.nix) and state # survives reboots, exactly like a machine you ran nixos/install.sh on. @@ -19,15 +19,15 @@ # # This host is independent of nixos/install.sh; it shares the disk LAYOUT with # real installs (disko-standard.nix) but is never itself part of the install -# flow. The turn-key login + Coder admin bootstrap (shared with the live ISO) -# live in nixos/box-turnkey.nix. +# flow. The turn-key login + Coder admin bootstrap (shared with the appliance ISO) +# live in nixos/_appliance/box-turnkey.nix. { lib, pkgs, ... }: { imports = [ ../../nixos/disko-standard.nix # 1 GB ESP + ext4 root single-disk layout - ../../nixos/box-turnkey.nix # shared turn-key config (login + Coder bootstrap) + ../../nixos/_appliance/box-turnkey.nix # shared turn-key config (login + Coder bootstrap) ] ++ lib.optional (builtins.pathExists ./local.nix) ./local.nix; # No networking.hostName here on purpose: underscore-prefixed image hosts get diff --git a/hosts/_appliance_iso/default.nix b/hosts/_appliance_iso/default.nix index 5abe6bc..0453454 100644 --- a/hosts/_appliance_iso/default.nix +++ b/hosts/_appliance_iso/default.nix @@ -10,8 +10,8 @@ # # Unlike the install hosts (coder-thinkcentre, qemu-arm64), this host does NOT # import nixos/disko-standard.nix, hardware-configuration.nix, or facter.json: -# the live root filesystem is the squashfs + tmpfs overlay provided by -# nixos/live-iso.nix. All of the live-specific wiring lives in that module. +# the appliance root filesystem is the squashfs + tmpfs overlay provided by +# nixos/_appliance/live-iso.nix. All of the appliance-ISO wiring lives there. # # This host is independent of nixos/install.sh and never participates in the # disk-install flow; adding it changes nothing for disko/nixos-install installs. @@ -19,7 +19,7 @@ { lib, ... }: { - imports = [ ../../nixos/live-iso.nix ] + imports = [ ../../nixos/_appliance/live-iso.nix ] ++ lib.optional (builtins.pathExists ./local.nix) ./local.nix; # No networking.hostName here on purpose: underscore-prefixed image hosts get diff --git a/nixos/box-turnkey.nix b/nixos/_appliance/box-turnkey.nix similarity index 95% rename from nixos/box-turnkey.nix rename to nixos/_appliance/box-turnkey.nix index 87cb17e..3304834 100644 --- a/nixos/box-turnkey.nix +++ b/nixos/_appliance/box-turnkey.nix @@ -1,9 +1,9 @@ # Shared "turn-key" Box™ config — the bits that make an image boot straight # into a fully-configured, ready-to-use Coder box with no install step. # -# Imported by both image flavours: -# - nixos/live-iso.nix (live, ephemeral ISO: hosts/live) -# - hosts/persistent-disk/ (persistent disk image: qcow2 / raw) +# Imported by both appliance flavours: +# - nixos/_appliance/live-iso.nix (ephemeral appliance ISO: hosts/_appliance_iso) +# - hosts/_appliance-disk/ (persistent disk image: qcow2 / raw) # # On real installs these settings come from nixos/install.sh + the gitignored # hosts//local.nix it generates. The image flavours have no install step, diff --git a/nixos/live-iso.nix b/nixos/_appliance/live-iso.nix similarity index 71% rename from nixos/live-iso.nix rename to nixos/_appliance/live-iso.nix index ac63fba..028093a 100644 --- a/nixos/live-iso.nix +++ b/nixos/_appliance/live-iso.nix @@ -1,26 +1,27 @@ -# Live "Box" ISO module — "it's just The Box™", not an installer. +# Appliance ISO module — "it's just The Box™", not an installer. # -# Turns the shared Coder box configuration into a bootable *ephemeral* live ISO -# that runs entirely from the USB/CD + RAM, with no disk install. Booting it +# Turns the shared Coder box configuration into a bootable *ephemeral* appliance +# ISO that runs entirely from the USB/CD + RAM, with no disk install. Booting it # gives the same system the on-disk install produces (KDE, Coder server, k3s, # Podman, the bundled templates, all started automatically) — but the root # filesystem is a squashfs + tmpfs overlay, so all state is discarded on -# reboot. For a *persistent* image (state survives reboots) build the -# persistent-disk host instead (qcow2 / raw); see the Makefile / README. +# reboot. For a *persistent* appliance (state survives reboots) build the +# _appliance-disk host instead (qcow2 / raw); see the Makefile / README. # -# Build (folder name `live` => nixosConfigurations.live, see flake.nix): +# Build (hosts/_appliance_iso => nixosConfigurations._appliance_iso, see flake.nix): # -# make live-ephemeral-iso -# # or: nix build .#nixosConfigurations.live.config.system.build.isoImage -# # → result/iso/coder-box-appliance-*.iso (flash with `dd`, Ventoy, etc.) +# make appliance/iso +# # or: nix build .#nixosConfigurations._appliance_iso.config.system.build.isoImage +# # → out/appliance-iso/iso/coder-box-appliance-*.iso (flash with `dd`, Ventoy, etc.) # -# This module is imported only by hosts/live/default.nix and is independent of -# the regular disk-install flow (nixos/install.sh, disko, nixos-facter). It -# imports NO disko / hardware-configuration.nix / facter.json: the live root is -# the squashfs + tmpfs overlay that nixpkgs' iso-image.nix sets up. +# This module is imported only by hosts/_appliance_iso/default.nix and is +# independent of the regular disk-install flow (nixos/install.sh, disko, +# nixos-facter). It imports NO disko / hardware-configuration.nix / facter.json: +# the appliance root is the squashfs + tmpfs overlay that nixpkgs' iso-image.nix +# sets up. # -# The turn-key login + Coder admin bootstrap (shared with the persistent-disk -# image) live in nixos/box-turnkey.nix. +# The turn-key login + Coder admin bootstrap (shared with the _appliance-disk +# image) live in nixos/_appliance/box-turnkey.nix. { config, lib, pkgs, modulesPath, ... }: @@ -38,7 +39,7 @@ # ── ISO image settings ────────────────────────────────────────────────────── isoImage.makeEfiBootable = true; # boot on UEFI machines # Legacy BIOS boot uses syslinux, which is x86-only. Enable it just for x86 - # so the same module also evaluates/builds for an aarch64 live ISO (which + # so the same module also evaluates/builds for an aarch64 appliance ISO (which # boots via EFI only). isx86 covers both i686 and x86_64. isoImage.makeBiosBootable = pkgs.stdenv.hostPlatform.isx86; isoImage.makeUsbBootable = true; # `dd` straight to a USB stick and boot From 9d1641a2a9a326230521e7ffefdc0fea81472c81 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Mon, 8 Jun 2026 10:31:13 +0000 Subject: [PATCH 17/19] fix(appliance): make terraform workdir writable so templates deploy on first boot coder-init-admin.service copies coderd/ out of /etc/nixos-repo into a workdir and runs terraform there. On the appliance images /etc/nixos-repo is a read-only Nix store path (dirs 0555, files 0444), so 'cp -r' reproduces those read-only perms and 'terraform init' fails writing .terraform.lock.hcl into the workdir: cannot create temporary file to update .terraform.lock.hcl: ... permission denied Under 'set -o pipefail' that aborts the service *after* the admin user + token were already created, so templates silently never deploy (admin works, no templates). Add 'chmod -R u+w $CODERD_DIR' after the copy. On normal installs the source is already writable, so this is a harmless no-op. Verified: reproduced the read-only-workdir failure from the baked store path, and confirmed chmod -R u+w makes 'terraform init' able to write its lock file. --- configuration.nix | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/configuration.nix b/configuration.nix index 983e839..c6f6310 100644 --- a/configuration.nix +++ b/configuration.nix @@ -440,8 +440,18 @@ in # it can't write the .terraform.lock.hcl that terraform init # creates in the working directory. Copy coderd/ into a workdir # we own and run terraform there. + # + # On the appliance images /etc/nixos-repo is a read-only Nix store + # path (dirs 0555, files 0444), so `cp -r` reproduces those + # read-only perms and `terraform init` then fails writing + # .terraform.lock.hcl into the workdir (Permission denied) — which, + # under `set -o pipefail`, aborts this service *after* the admin + # user + token were already created, so templates silently never + # deploy. chmod -R u+w makes the copy writable. (On normal installs + # the source is already writable, so this is a harmless no-op.) rm -rf "$CODERD_DIR" cp -r "$CODERD_SRC" "$CODERD_DIR" + chmod -R u+w "$CODERD_DIR" COMMIT=$(GIT_DIR=/etc/nixos-repo/.git ${pkgs.git}/bin/git -c safe.directory=/etc/nixos-repo -C /etc/nixos-repo rev-parse --short HEAD 2>/dev/null || echo "unknown") export TF_CLI_CONFIG_FILE="${terraformrc}" export TF_DATA_DIR="$STATE_DIR/.terraform" From 841f201e5840a6758592313cd4a42d041c7ed27e Mon Sep 17 00:00:00 2001 From: Phorcys <57866459+phorcys420@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:36:24 +0200 Subject: [PATCH 18/19] chore: unify default pw --- nixos/_appliance/box-turnkey.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/_appliance/box-turnkey.nix b/nixos/_appliance/box-turnkey.nix index 3304834..b6c8fc5 100644 --- a/nixos/_appliance/box-turnkey.nix +++ b/nixos/_appliance/box-turnkey.nix @@ -70,7 +70,7 @@ description = "coderbox"; extraGroups = [ "networkmanager" "wheel" ]; packages = [ pkgs.kdePackages.kate ]; - initialPassword = "coderbox"; + initialPassword = "PleaseChangeMe1234"; }; # coder-init-admin.service reads CODER_ADMIN_* from coder.service's From 9ec8e6de122e1ba7a6ba44f920b10be65651aac7 Mon Sep 17 00:00:00 2001 From: Phorcys <57866459+phorcys420@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:39:30 +0200 Subject: [PATCH 19/19] Update live-iso.nix --- nixos/_appliance/live-iso.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/_appliance/live-iso.nix b/nixos/_appliance/live-iso.nix index 028093a..99ec2a8 100644 --- a/nixos/_appliance/live-iso.nix +++ b/nixos/_appliance/live-iso.nix @@ -43,7 +43,7 @@ # boots via EFI only). isx86 covers both i686 and x86_64. isoImage.makeBiosBootable = pkgs.stdenv.hostPlatform.isx86; isoImage.makeUsbBootable = true; # `dd` straight to a USB stick and boot - isoImage.volumeID = "BOX_LIVE"; + isoImage.volumeID = "CODER_BOX_LIVE"; # Boot-menu label (both the BIOS/isolinux and EFI/grub entries). The label is # " "; the default append is # " Installer", which is misleading here since this is the live appliance, not