Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fa50de9
feat: add live box ISO (hosts/live) + Makefile build targets
phorcys420 Jun 5, 2026
4b24ec2
refactor: rename live ISO target + add persistent-disk image (qcow2/raw)
phorcys420 Jun 5, 2026
7079c43
refactor: unify image build targets under appliance/<format>[/<arch>]
phorcys420 Jun 5, 2026
da8c228
appliance/iso: name the ISO coder-box-appliance.iso
phorcys420 Jun 5, 2026
078ac55
fix(make): bare appliance/* targets build for the builder's native arch
phorcys420 Jun 5, 2026
1ba7de1
make: emit appliance images into ./out via --out-link (no copy)
phorcys420 Jun 5, 2026
3389584
appliance/iso: include arch in the ISO file name
phorcys420 Jun 5, 2026
dc91f64
appliance/{raw,qcow2}: include arch in the disk image file name
phorcys420 Jun 5, 2026
f0989b2
chore: gitignore appliance image artifacts (*.iso, *.qcow2, *.raw)
phorcys420 Jun 5, 2026
20ef1f1
appliance/iso: boot-menu label 'NixOS <version> - Coder Box Appliance'
phorcys420 Jun 5, 2026
b013190
refactor: rename appliance host dirs to hosts/_appliance_iso and host…
phorcys420 Jun 5, 2026
3140f83
feat: default hostname to coder-box centrally; appliances inherit it
phorcys420 Jun 5, 2026
4983902
fix: stop appliance ISO growing every build (filter build artifacts f…
phorcys420 Jun 5, 2026
8e4047f
docs: mark qcow2/raw disk appliances as untested; fix stale host names
phorcys420 Jun 8, 2026
9b0b1de
docs: finish renaming stale live/persistent-disk host refs in README
phorcys420 Jun 8, 2026
b84b169
refactor: move appliance modules under nixos/_appliance/; 'Live ISO' …
phorcys420 Jun 8, 2026
9d1641a
fix(appliance): make terraform workdir writable so templates deploy o…
phorcys420 Jun 8, 2026
841f201
chore: unify default pw
phorcys420 Jun 8, 2026
9ec8e6d
Update live-iso.nix
phorcys420 Jun 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ hosts/*/local.nix
*.tfstate*
.terraform/
*.bak

# Appliance image build outputs (Makefile --out-link target dir + artifacts)
out/
*.iso
*.qcow2
*.raw
76 changes: 76 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Coder box — appliance image build targets.
#
# 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 # 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)
#
# Each format also takes an architecture suffix; short names are normalized to
# a *-linux triple (e.g. aarch64 -> aarch64-linux):
#
# 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.
# 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 format/arch is just a thin target below — no duplicated nix
# plumbing. We ALWAYS pin nixpkgs.hostPlatform: when no arch is given we use
# `builtins.currentSystem` (the builder's native arch), otherwise the bare
# `appliance/<format>` 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.<host>)
# $(2) = system.build.<attr> (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/<link> points
# straight at the store path, and being a GC root it won't be garbage-collected.
# ./out is gitignored.
define box_build
@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

.PHONY: appliance/iso appliance/qcow2 appliance/raw

# ── appliance/iso — ephemeral appliance ISO (hosts/_appliance_iso) ───────────
appliance/iso:
$(call box_build,_appliance_iso,isoImage,,)
appliance/iso/%:
$(call box_build,_appliance_iso,isoImage,,$*)

# ── appliance/qcow2 — persistent disk image (hosts/_appliance-disk) ──────────
appliance/qcow2:
$(call box_build,_appliance-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,)
appliance/qcow2/%:
$(call box_build,_appliance-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,$*)

# ── appliance/raw — persistent disk image, dd-able (hosts/_appliance-disk) ────
appliance/raw:
$(call box_build,_appliance-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,)
appliance/raw/%:
$(call box_build,_appliance-disk,diskoImages,disko.imageBuilder.imageFormat = "raw";,$*)
105 changes: 101 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ NixOS configuration for Coder demo and workshop boxes.
flake.nix # entry point: nixosConfigurations.<host> per machine
flake.lock # pinned nixpkgs / disko / nixos-facter-modules
configuration.nix # shared NixOS config (all machines)
Makefile # appliance build targets: appliance/{iso,qcow2,raw}[/<arch>]
local.nix.example # template copied to hosts/<host>/local.nix by install.sh
.gitignore # ignores hosts/*/local.nix
nixos/
Expand All @@ -38,6 +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
_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
Expand All @@ -49,6 +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 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 + nixos/_appliance/box-turnkey.nix
coderd/
main.tf # manages all Coder templates via coderd Terraform provider
templates/
Expand All @@ -60,10 +68,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.<folder-name>`. 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.<folder-name>`. 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:

Expand Down Expand Up @@ -108,6 +121,90 @@ The installer generates `hosts/<hostname>/{default.nix,local.nix,facter.json}`,
> ```
> And use a BIOS-compatible disko layout instead of `disko-standard.nix`.

## Prebuilt images (The Box™ without `install.sh`)

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.

These prebuilt images are called **appliances** (the box, prebuilt — no
`install.sh`). Build them with `make appliance/<format>`:

| 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
builds need a matching builder (native remote builder or binfmt/QEMU):

```sh
make appliance/iso/aarch64-linux
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`). 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/_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://<hostname>.local:3000` (or the `*.try.coder.app` tunnel URL in
`/etc/motd`). Change these before sharing an image by dropping a gitignored
`hosts/<host>/local.nix` (same shape as `local.nix.example`).

### Appliance ISO (`_appliance_iso`)

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/_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
aarch64 ISO is EFI-only). Flash it (it's isohybrid) and boot:

```sh
sudo dd if=out/appliance-iso/iso/coder-box-appliance-*.iso of=/dev/sdX bs=4M status=progress oflag=sync
```

### 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
ESP + ext4 root) and **state survives reboots**, exactly like a machine you ran
`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
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 `_appliance-disk` host shares only the disk *layout*
(`disko-standard.nix`) with real installs, never the install process itself.

## After install

The installer auto-creates the admin user, mints a long-lived API token to
Expand Down
7 changes: 7 additions & 0 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,17 @@ sudo k3s kubectl describe pod -n coder-workspaces <pod-name>
k3s-sysbox.nix # k3s + sysbox runtime
k3s-podman.nix # k3s + rootless Podman socket
screenconnect.nix # ScreenConnect remote access client
_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
hosts/
_appliance_iso/ # `_appliance_iso` host: ephemeral live "Box" ISO; no disko/facter/hardware-config
# build: make appliance/iso (or appliance/iso/<arch>)
_appliance-disk/ # `_appliance-disk` host: persistent qcow2/raw disk image (disko image builder)
# build: make appliance/qcow2 | make appliance/raw (or .../<arch>)
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)
Expand Down
26 changes: 23 additions & 3 deletions configuration.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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/<host>/local.nix or default.nix.
# Central default hostname. Install hosts override this: flake.nix's mkHost
# injects `networking.hostName = lib.mkDefault <folder-name>` 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 <hostname>.local on the LAN
Expand Down Expand Up @@ -430,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"
Expand Down
23 changes: 17 additions & 6 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 `.#<attr>`. Adding a new host means
# just creating ./hosts/<hostname>/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 `.#<attr>`. Adding a new host
# means just creating ./hosts/<hostname>/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"
Expand All @@ -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 =
Expand Down
Loading