Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 # 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 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 — live ephemeral 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";,$*)
97 changes: 93 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,8 @@ 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)
pkgs/
coder.nix # custom Coder server package
coderd-provider.nix # terraform-provider-coderd package
Expand All @@ -49,6 +52,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-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
templates/
Expand All @@ -60,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.<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 +120,83 @@ 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 | 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` |

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/box-turnkey.nix`](nixos/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`).

### 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/_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
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 (`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/_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 `persistent-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
6 changes: 6 additions & 0 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,16 @@ 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
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)
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
16 changes: 13 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
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
57 changes: 57 additions & 0 deletions hosts/_appliance-disk/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 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._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 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._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
# 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, 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)
] ++ lib.optional (builtins.pathExists ./local.nix) ./local.nix;

# 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
# (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";

# Output file name: disko defaults imageName to the disk attr name ("main"),
# 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-<arch>.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
# 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;
}
28 changes: 28 additions & 0 deletions hosts/_appliance_iso/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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._appliance_iso`.
# It's normally built via the Makefile rather than by attribute:
#
# 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:
# 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;

# 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.
}
Loading