https://github.com/coder/box
The Box™ is a NixOS-based appliance for Coder
https://github.com/coder/box
nixos
Last synced: 1 day ago
JSON representation
The Box™ is a NixOS-based appliance for Coder
- Host: GitHub
- URL: https://github.com/coder/box
- Owner: coder
- Created: 2026-04-15T17:08:16.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-06-29T20:10:58.000Z (14 days ago)
- Last Synced: 2026-06-29T21:09:24.871Z (14 days ago)
- Topics: nixos
- Language: Nix
- Homepage:
- Size: 5.14 MB
- Stars: 7
- Watchers: 0
- Forks: 1
- Open Issues: 15
-
Metadata Files:
- Readme: README.md
- Agents: agents.md
Awesome Lists containing this project
- awesome-coder - coder/box - NixOS appliance that provisions a single-node Coder server and k3s cluster on a physical machine for self-contained demos and workshops. (Automation)
README
NixOS configuration for Coder demo and workshop boxes.
> **Demo box setup**. This repo configures one or more single-purpose physical machines running Coder + k3s as self-contained workshop and demo environments. It is intentionally simple: no HA, no remote state, no cloud provider. Each machine's secrets (IPs, auth keys, passwords) live in a per-host gitignored `hosts//local.nix` file. Each box runs a Coder server, k3s (single-node), and a set of workspace templates managed by Terraform via the `coderd` provider.
This repo is a Nix flake. `flake.nix` auto-discovers every subdirectory of
`./hosts/` that contains a `default.nix` and exposes it as
`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`, `_installer-iso`) are image 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:
- [`disko`](https://github.com/nix-community/disko) declares partition layouts in Nix. `nixos/disko-standard.nix` is a single-disk UEFI layout (1 GB EFI / ZFS root pool; no on-disk swap — zram instead). `install.sh` picks the device at install time; the ZFS `networking.hostId` is derived in Nix from the hostname (sha256 substring), so each host gets a distinct id automatically. ZFS gives cheap on-demand snapshots (take one before a risky rebuild and roll back in seconds), zstd compression, and checksum/scrub integrity.
- [`nixos-facter`](https://github.com/nix-community/nixos-facter) writes a JSON hardware report (`facter.json`) that replaces `hardware-configuration.nix` on new hosts. The `nixos-facter-modules` module reads it to set kernel modules, microcode, GPU drivers, and so on.
## Installing on a new machine
From a NixOS live USB on the target box, with network access (any reasonably recent ISO from [nixos.org](https://nixos.org/download/) works; the installed system pins its own nixpkgs in `flake.lock` independent of what the USB is running):
```sh
nix-shell -p git --run "git clone https://github.com/coder/box /tmp/box"
cd /tmp/box
sudo ./install.sh --interactive
```
With `--interactive` (`-i`) the installer prompts for any value not already passed as a flag, including a target disk picker. Each prompt offers a default; press Enter to accept it. Without `--interactive`, a target disk must be given via `--disk` (there is no safe default). Other values not passed as a flag fall back to a default: hostname `coder-box-` (e.g. `coder-box-deadbeef`), Coder admin `admin@coder.com` / `PleaseChangeMe1234`, OS login `coderbox` / `PleaseChangeMe1234`. Passwords in the summary are obfuscated, unless they are left as defaults.
For a fully unattended install, pass every value as a flag:
```sh
sudo ./install.sh \
--hostname coder-demo \
--disk /dev/nvme0n1 \
--coder-admin-email you@example.com \
--coder-admin-password 'changeme' \
--nixos-username coderbox \
--nixos-password 'changeme' \
--yes
```
`./install.sh --help` lists everything. `--coder-admin-password-file PATH` and `--nixos-password-file PATH` read passwords from a file so they don't end up in shell history. `--no-reboot` skips the automatic reboot at the end.
The installer generates `hosts//{default.nix,local.nix,facter.json}`, copies the repo into `/etc/nixos-repo` on the target, and symlinks `/etc/nixos/flake.nix`. After reboot, `nixos-rebuild switch` Just Works. Continue with [After install](#after-install).
> **Different partition layout?** Don't import `nixos/disko-standard.nix`; drop your own disko config into the host folder instead. See [disko examples](https://github.com/nix-community/disko/tree/master/example).
> **BIOS hardware?** The shared config defaults to `systemd-boot` (UEFI). In your host's `default.nix`:
> ```nix
> boot.loader.systemd-boot.enable = false;
> boot.loader.grub = { enable = true; device = "/dev/sda"; };
> ```
> 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 — GNOME, 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 | 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`.
Every shipped image — ISO, qcow2, and raw — is built alongside a `.sha256`
sidecar in the same directory (`coder-box-appliance-*.iso.sha256`,
`coder-box-appliance-*.qcow2.sha256`, etc.), so `sha256sum -c .sha256`
verifies the image and `cp -L out//*` (or `.../iso/*` for ISOs) copies
image + checksum together. The build prints each checksum when it finishes.
The turn-key login + Coder admin bootstrap shared by all image flavours live in
[`nixos/_images/box-turnkey.nix`](nixos/_images/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`).
### 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/_images/appliance/iso.nix`](nixos/_images/appliance/iso.nix) (which pulls in `base/iso.nix` + `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 + ZFS root pool) 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
```
All image hosts are completely separate from the disk-install flow above
(`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.
### Installer ISO (`_installer-iso`)
The installer is the box as an ISO whose eventual job is to install `coder/box`
onto real hardware. **For now it is identical to the appliance ISO** (full GUI
box + turn-key Coder bootstrap), differing only in image identity (volume ID
`CODER_BOX_INSTALLER`, boot-menu label, and file name
`coder-box-installer-.iso`); the minimal/installer-only environment is a
future change. It builds **only as an ISO** (no qcow2/raw):
```sh
make installer/iso # native arch
make installer/iso/aarch64-linux # explicit arch
# → out/installer-iso/iso/coder-box-installer-*.iso
```
`hosts/_installer-iso/default.nix` imports
[`nixos/_images/installer/iso.nix`](nixos/_images/installer/iso.nix), which —
like the appliance ISO — pulls in `base/iso.nix` + `box-turnkey.nix`.
## After install
The installer auto-creates the admin user, mints a long-lived API token to
`/etc/coder/session-token`, and deploys the workspace templates on first
boot via `coder-init-admin.service`. After the reboot:
1. Find the box at `http://.local:3000`, or look up the
`*.try.coder.app` tunnel URL in `/etc/motd` on the box (also tailed to
the console on each SSH login).
2. Log in with the Coder admin email and password set at install time
(defaults: `admin@coder.com` / `PleaseChangeMe1234`).
3. Change the admin password from the user settings page if you used the
defaults.
Subsequent edits to `coderd/` templates go out via `coder-template-sync`
on every `sudo nixos-rebuild switch`.
## Applying changes
```sh
sudo nixos-rebuild switch # most changes
sudo nixos-rebuild boot && sudo reboot # changes that touch the desktop stack
# Edited hosts//local.nix or facter.json? Re-mark intent-to-add:
sudo git -C /etc/nixos-repo add --intent-to-add -f \
hosts//local.nix \
hosts//facter.json
```
## Updating nixpkgs / disko / facter
```sh
sudo nix flake update --flake /etc/nixos-repo
sudo nixos-rebuild switch
```
This bumps `flake.lock` to the latest of each input.
## Workspace Templates
### coder-cli
Full-featured CLI/dev workspace running the `codercom/oss-dogfood` image (Ubuntu + docker CLI, terraform, gh, go, node, etc.). No sysbox or inner Docker daemon; uses the host's runtime.
### k3s-sysbox
Full Docker-in-workspace via sysbox-runc. Each workspace gets an isolated Docker daemon. No privileged mode on the host.
### k3s-podman
Docker-compatible socket via host rootless Podman. Simpler than sysbox; no inner daemon. `docker` CLI works via `DOCKER_HOST`.
### k3s-dev
Language demo workspaces. Pick Python/Node.js/Go/Java/Rust at creation; a real demo app auto-starts (FastAPI, Next.js, Pagoda, Spring PetClinic, rustypaste).
### nook-android *(thinkcentre only)*
Dev environment for building the [trmnl-nook-simple-touch](https://github.com/usetrmnl/trmnl-nook-simple-touch) APK for the Barnes & Noble Nook Simple Touch. Uses a pre-built image (`localhost/nook-android:latest`) loaded into k3s by the `nook-android-image-build` NixOS service. 32-bit ADT tools run transparently via qemu-i386 binfmt.
## Key Services
| Service | Description |
|---|---|
| `coder.service` | Coder server on port 3000 |
| `postgresql.service` | Local PostgreSQL for Coder |
| `k3s.service` | Single-node k3s (sysbox-runc runtime) |
| `tailscaled` + `tailscale-autoauth` | Tailscale (auth key in `hosts//local.nix`) |
| `nook-android-image-build` | Builds/imports nook-android image into k3s containerd |
| `coder-sync-ssh-keys` | Fetches SSH keys from GitHub on boot |
| `screenconnect-install` | Downloads and installs ScreenConnect client (oneshot) |
| `screenconnect` | ScreenConnect remote access daemon |
| `coder-redirect.service` | HTTP 302 redirect: port 80 → live `*.try.coder.app` tunnel URL |
## Reset / Full Wipe
Encoded as a NixOS systemd service. No manual steps needed.
```sh
sudo systemctl start coder-reset
```
Fully automated, no follow-up steps needed. The service:
1. Stops Coder, coder-redirect
2. Force-deletes all workspace pods and PVCs from k3s
3. Drops and recreates the PostgreSQL database
4. Wipes `/var/lib/coder` (data dir, sentinel, tokens, Podman volumes)
5. Starts Coder and waits for the API
6. Re-bootstraps the admin user from credentials in the host's `local.nix`
7. Mints a fresh long-lived session token → writes to `/etc/coder/session-token`
8. Restarts `coder-redirect`
9. Runs `nixos-rebuild switch` to push templates back via `coder-template-sync`
### Changing the admin password
1. Edit `hosts//local.nix`, update `CODER_ADMIN_PASSWORD`.
2. Run `sudo nixos-rebuild switch` to bake the new password into the service.
3. Run `sudo systemctl start coder-reset` to wipe and re-bootstrap with the new password.
> If you need to change the password on a **live** deployment without a full wipe:
> ```sh
> TOKEN=$(curl -sf -X POST http://localhost:3000/api/v2/users/login \
> -H 'Content-Type: application/json' \
> -d '{"email":"admin@coder.com","password":""}' | jq -r '.session_token')
> curl -sf -X PUT http://localhost:3000/api/v2/users/me/password \
> -H "Coder-Session-Token: $TOKEN" \
> -H 'Content-Type: application/json' \
> -d '{"old_password":"","password":""}'
> ```
## Notes
- Steps 1 to 4 run while Coder is stopped so the provisioner can't re-create pods mid-wipe; the systemd service handles the stop and restart.
- If the admin password changed before a reset and the user already exists in Postgres (rare; the wipe drops the DB), update via the API instead of `coder-reset`:
```sh
TOKEN=$(curl -sf -X POST http://localhost:3000/api/v2/users/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@coder.com","password":""}' \
| jq -r '.session_token')
curl -sf -X PUT http://localhost:3000/api/v2/users/me/password \
-H "Coder-Session-Token: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"old_password":"","password":""}'
```
- Rootless Podman volumes live under `/var/lib/coder/.local/...`; `rm -rf /var/lib/coder/*` in step 4 clears these too.
- k3s PVs backed by `local-path-provisioner` live under `/var/lib/rancher/k3s/storage/`; `kubectl delete pvc` in step 2 triggers their cleanup.
## Repo notes
- `hosts//local.nix` is gitignored. Never commit secrets or machine-specific overrides.
- The `coderd/` Terraform state is stored in `/var/lib/coder/template-sync/` on the box, not in the repo.
- `CODER_ACCESS_URL` is intentionally unset; Coder auto-creates a `*.try.coder.app` tunnel on startup. `http://.local` (port 80) redirects to the live tunnel URL via `coder-redirect.service`, which also writes the URL to `/etc/motd` so it shows on every console and SSH login.
- The `coder` user (uid 991) runs Coder server and rootless Podman. UID is pinned; do not change.
- Workspace pods resolve `.local` via a `hostAliases` entry pointing to the LAN IP (set via `services.coder-nixos.lanIp` in the host's `local.nix`).