Skip to content
Merged
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
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ sudo ./install.sh \

`./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/<hostname>/{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).
The installer generates `hosts/<hostname>/{default.nix,local.nix,install-answers.json,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).
> **Different partition layout?** Don't import `installer/bootstrap/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
Expand Down Expand Up @@ -117,7 +117,7 @@ The turn-key login + Coder admin bootstrap shared by all image flavours live in
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`).
`hosts/<host>/local.nix` (same shape as `installer/bootstrap/local.nix.example`).

### Appliance ISO (`_appliance-iso`)

Expand All @@ -144,7 +144,7 @@ sudo dd if=out/appliance-iso/iso/coder-box-appliance-*.iso of=/dev/sdX bs=4M sta
> 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
it carries the real on-disk GPT layout from `installer/bootstrap/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`.
Expand Down Expand Up @@ -204,9 +204,10 @@ on every `sudo nixos-rebuild switch`.
sudo nixos-rebuild switch # most changes
sudo nixos-rebuild boot && sudo reboot # changes that touch the desktop stack

# Edited hosts/<host>/local.nix or facter.json? Re-mark intent-to-add:
# Edited hosts/<host>/local.nix, install-answers.json, or facter.json? Re-mark intent-to-add:
sudo git -C /etc/nixos-repo add --intent-to-add -f \
hosts/<host>/local.nix \
hosts/<host>/install-answers.json \
hosts/<host>/facter.json
```

Expand Down Expand Up @@ -265,14 +266,14 @@ Fully automated, no follow-up steps needed. The service:
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`
6. Re-bootstraps the initial user from `services.coder-nixos.initialUser` (set in the host's `install-answers.json`)
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/<host>/local.nix`, update `CODER_ADMIN_PASSWORD`.
1. Edit `hosts/<host>/install-answers.json`, update `initialUser.password` (or override `services.coder-nixos.initialUser.password` in `local.nix`).
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.

Expand Down
9 changes: 6 additions & 3 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ sudo k3s kubectl describe pod -n coder-workspaces <pod-name>
- **Template sync skips**, if `/etc/coder/session-token` is empty, the activation script exits cleanly. The token is auto-populated by `coder-init-admin.service`; if it's missing, check `journalctl -u coder-init-admin`.
- **`coder` binary path** — the binary is in PATH via NixOS environment; don't hardcode nix store paths in scripts (they change with every package update).
- **`--flake /etc/nixos` fails** — `/etc/nixos` is a plain dir holding only a `flake.nix` *symlink* into `/etc/nixos-repo`. Nix follows the symlink into the store but can't find the sibling files (configuration.nix, hosts/, nixos/), dying with `path '/nix/store/...-source/etc/nixos-repo/flake.nix' does not exist`. Always rebuild against the real tree: `--flake /etc/nixos-repo` (or `cd /etc/nixos-repo && nixos-rebuild switch --flake .`).
- **`Git tree '/etc/nixos-repo' is dirty` warning** — harmless. `hosts/<host>/{local.nix,facter.json}` are gitignored and intent-to-added by the installer, so the tree always reads "dirty". After editing them, re-mark intent-to-add so the flake sees them: `sudo git -C /etc/nixos-repo add --intent-to-add -f hosts/<host>/local.nix hosts/<host>/facter.json`.
- **`Git tree '/etc/nixos-repo' is dirty` warning** — harmless. `hosts/<host>/{local.nix,install-answers.json,facter.json}` are gitignored and intent-to-added by the installer, so the tree always reads "dirty". After editing them, re-mark intent-to-add so the flake sees them: `sudo git -C /etc/nixos-repo add --intent-to-add -f hosts/<host>/local.nix hosts/<host>/install-answers.json hosts/<host>/facter.json`.
- **ScreenConnect blank screen** — the box now runs GNOME on Wayland (GDM), and GNOME 49 dropped the Xorg session, so there is no X11 desktop to fall back to. ScreenConnect reaches `DISPLAY=:0` through XWayland (see `nixos/screenconnect.nix`) but **cannot screen-capture the Wayland compositor** through it, so the remote view may be black/blank. Capturing the GNOME session needs a Wayland-aware path (PipeWire/portal, e.g. `gnome-remote-desktop`); the X11 agent will connect but not mirror the desktop.

## Wildcard App Access (TODO)
Expand All @@ -216,9 +216,12 @@ sudo k3s kubectl describe pod -n coder-workspaces <pod-name>
flake.nix # entry point: nixosConfigurations.<host> per machine
flake.lock # pinned nixpkgs / disko
configuration.nix # shared NixOS config (edit here for services/packages)
local.nix.example # template for hosts/<host>/local.nix
install.sh # live-USB installer (writes hosts/<host>/{default.nix,local.nix,install-answers.json,facter.json})
installer/
bootstrap/ # assets install.sh consumes when generating a new host
disko-standard.nix # shared disko config: UEFI + single-disk layout for new hosts
local.nix.example # template copied to hosts/<host>/local.nix; reads install-answers.json and applies the values
nixos/
disko-standard.nix # shared disko config: UEFI + single-disk layout for new hosts
modules/ # NixOS service modules (services.coder-nixos.*)
k3s/ # base single-node k3s server
podman/ # k3s + rootless Podman socket runtime
Expand Down
84 changes: 55 additions & 29 deletions configuration.nix
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,29 @@ in
description = "LAN IP of this box, used for CODER_AGENT_URL and k8s hostAliases so pods resolve the hostname without relying on mDNS. Set in the host's local.nix. Leave empty to fall back to hostname-based mDNS URL.";
};

# ── NixOS option: Coder initial user ───────────────────────────────────────
# The owner account coder-init-admin.service creates on first boot. Set from
# hosts/<host>/install-answers.json by the generated default.nix, or overridden
# in local.nix (optionally wired to a secret via agenix/sops). Plain strings,
# not systemd env overrides, so credentials live in one obvious place.
options.services.coder-nixos.initialUser = {
username = lib.mkOption {
type = lib.types.str;
default = "admin";
description = "Username of the Coder initial (owner) user created on first boot.";
};
email = lib.mkOption {
type = lib.types.str;
default = "";
description = "Email of the Coder initial user. Empty skips the bootstrap and leaves the browser first-run wizard to create the user.";
};
password = lib.mkOption {
type = lib.types.str;
default = "";
description = "Initial password for the Coder initial user. Change it after first login, or point it at a secret via agenix/sops.";
};
};

options.services.coder-sync-ssh-keys.githubUsers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
Expand Down Expand Up @@ -128,7 +151,7 @@ in
boot.loader.efi.canTouchEfiVariables = lib.mkDefault true;

# ── Filesystem: ZFS root ───────────────────────────────────────────────────
# The standard single-disk layout (nixos/disko-standard.nix) puts root on a
# The standard single-disk layout (installer/bootstrap/disko-standard.nix) puts root on a
# ZFS pool ("rpool"). The kernel needs the ZFS module available at boot to
# import it; declare it here so every host that follows the standard layout
# (and the prebuilt appliance images) boots. Hosts that predate disko and
Expand All @@ -152,7 +175,7 @@ in
services.zfs.trim.enable = lib.mkDefault true;

# ── Swap ──────────────────────────────────────────────────────────────────
# No on-disk swap partition (see nixos/disko-standard.nix). Use a
# No on-disk swap partition (see installer/bootstrap/disko-standard.nix). Use a
# compressed in-RAM swap device instead, sized to half of RAM.
zramSwap.enable = lib.mkDefault true;

Expand Down Expand Up @@ -304,9 +327,10 @@ in
services.printing.enable = true;

# ── Users ─────────────────────────────────────────────────────────────────
# Desktop / SSH login user is declared per-host in local.nix (template
# in local.nix.example); username and password are install-time flags.
# The `coder` system user (uid 991) is shared and declared further down.
# The desktop / SSH login user is declared per-host in local.nix from the
# install-answers.json values (username + initial password chosen at install
# time). The `coder` system user (uid 991) is shared and declared further
# down.

security.sudo.wheelNeedsPassword = false;

Expand Down Expand Up @@ -499,9 +523,9 @@ in
# ── Coder server ──────────────────────────────────────────────────────────
# Base env vars live here. Server secrets (e.g. OAuth) are merged in via
# systemd.services.coder.environment in hosts/<host>/local.nix; no
# EnvironmentFile. Admin bootstrap creds (CODER_ADMIN_*) are NOT set here —
# they live on coder-init-admin.service so they stay off the long-running
# server's environment.
# EnvironmentFile. Initial-user credentials are NOT set here — they come from
# the services.coder-nixos.initialUser option and are wired onto
# coder-init-admin.service below, so they stay off the long-running server.
systemd.services.coder = {
description = "Coder Server";
wantedBy = [ "multi-user.target" ];
Expand Down Expand Up @@ -549,22 +573,24 @@ in
};

# ── Admin user bootstrap ──────────────────────────────────────────────────
# Reads CODER_ADMIN_* from this service's own environment (set via
# local.nix as systemd.services.coder-init-admin.environment), keeping the
# admin credentials off the long-running coder.service.
# Creates a local admin account once; sentinel prevents re-running.
# If CODER_ADMIN_EMAIL is unset, skips and directs user to the browser wizard.
# Creates the Coder initial (owner) user once from the
# services.coder-nixos.initialUser option; a sentinel prevents re-running.
# If initialUser.email is unset, skips and directs the user to the browser
# wizard.
systemd.services.coder-init-admin = {
description = "Coder bootstrap: create admin, mint session token, deploy templates";
wantedBy = [ "multi-user.target" ];
after = [ "coder.service" ];
requires = [ "coder.service" ];

# Inherit the coder.service environment so CODER_PG_CONNECTION_URL (and the
# other server vars) are available without duplication. The CODER_ADMIN_*
# credentials are merged in on top via the coder-init-admin.environment
# definition in hosts/<host>/local.nix (NixOS merges attrset options).
inherit (config.systemd.services.coder) environment;
# other server vars) are available without duplication, then add the
# initial-user credentials from the services.coder-nixos.initialUser option.
environment = config.systemd.services.coder.environment // {
INITIAL_USER_USERNAME = config.services.coder-nixos.initialUser.username;
INITIAL_USER_EMAIL = config.services.coder-nixos.initialUser.email;
INITIAL_USER_PASSWORD = config.services.coder-nixos.initialUser.password;
};

serviceConfig = {
Type = "oneshot";
Expand All @@ -581,8 +607,8 @@ in
templates_sentinel=/var/lib/coder/.templates-deployed
token_file=/etc/coder/session-token

if [ -z "''${CODER_ADMIN_EMAIL:-}" ]; then
echo "CODER_ADMIN_EMAIL not set, skipping bootstrap."
if [ -z "''${INITIAL_USER_EMAIL:-}" ]; then
echo "INITIAL_USER_EMAIL not set, skipping bootstrap."
echo "Complete the first-run wizard at http://$(${pkgs.nettools}/bin/hostname -s).local:3000"
exit 0
fi
Expand All @@ -608,12 +634,12 @@ in
if [ -f "$admin_sentinel" ]; then
echo "Admin user already created."
else
echo "Creating admin user $CODER_ADMIN_EMAIL..."
echo "Creating admin user $INITIAL_USER_EMAIL..."
${coder}/bin/coder server create-admin-user \
--postgres-url "$CODER_PG_CONNECTION_URL" \
--username "$CODER_ADMIN_USERNAME" \
--email "$CODER_ADMIN_EMAIL" \
--password "$CODER_ADMIN_PASSWORD"
--username "$INITIAL_USER_USERNAME" \
--email "$INITIAL_USER_EMAIL" \
--password "$INITIAL_USER_PASSWORD"
touch "$admin_sentinel"
fi

Expand All @@ -625,7 +651,7 @@ in
echo "Logging in as admin to mint a long-lived token..."
SESSION=$(${pkgs.curl}/bin/curl -sf -X POST http://localhost:3000/api/v2/users/login \
-H 'Content-Type: application/json' \
-d "{\"email\":\"$CODER_ADMIN_EMAIL\",\"password\":\"$CODER_ADMIN_PASSWORD\"}" \
-d "{\"email\":\"$INITIAL_USER_EMAIL\",\"password\":\"$INITIAL_USER_PASSWORD\"}" \
| ${pkgs.jq}/bin/jq -r '.session_token')
[ -n "$SESSION" ] && [ "$SESSION" != "null" ] \
|| { echo "Admin login failed." >&2; exit 1; }
Expand Down Expand Up @@ -702,9 +728,9 @@ in
];
requires = [ "postgresql.service" ];

# Step 8 mints a session token using CODER_ADMIN_EMAIL/PASSWORD, so pull in
# the coder-init-admin environment (which itself includes the coder.service
# vars plus the CODER_ADMIN_* credentials from local.nix).
# Step 8 mints a session token using the initial user's credentials, so pull
# in the coder-init-admin environment (which includes the coder.service vars
# plus the INITIAL_USER_* credentials from the initialUser option).
inherit (config.systemd.services.coder-init-admin) environment;

serviceConfig = {
Expand Down Expand Up @@ -752,12 +778,12 @@ in
echo "--- bootstrapping admin user"
${pkgs.systemd}/bin/systemctl start coder-init-admin.service

# 8. Mint a fresh long-lived session token using the admin creds from local.nix
# 8. Mint a fresh long-lived session token using the initial user's creds
echo "--- minting session token"
SESSION=$(${pkgs.curl}/bin/curl -sf \
-X POST http://localhost:3000/api/v2/users/login \
-H 'Content-Type: application/json' \
-d "{\"email\":\"''${CODER_ADMIN_EMAIL}\",\"password\":\"''${CODER_ADMIN_PASSWORD}\"}" \
-d "{\"email\":\"''${INITIAL_USER_EMAIL}\",\"password\":\"''${INITIAL_USER_PASSWORD}\"}" \
| ${pkgs.jq}/bin/jq -r '.session_token')
LONG_TOKEN=$(CODER_URL=http://localhost:3000 CODER_SESSION_TOKEN="$SESSION" \
${coder}/bin/coder tokens create --name nixos-sync --lifetime 8760h)
Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";

# Declarative disk partitioning. The repo ships a single-disk UEFI
# layout under nixos/disko-standard.nix that hosts can import.
# layout under installer/bootstrap/disko-standard.nix that hosts can import.
# install.sh runs `disko --mode disko` then `nixos-install`,
# which builds the closure directly into /mnt/nix/store on the target
# (avoids the tmpfs OOM that the `disko-install` one-shot hits on
Expand Down
4 changes: 2 additions & 2 deletions hosts/_appliance-disk/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# auto-discovery), so this host is exposed as `nixosConfigurations._appliance-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 + ZFS root pool from nixos/disko-standard.nix) and state
# GPT layout (1 GB ESP + ZFS root pool from installer/bootstrap/disko-standard.nix) and state
# survives reboots, exactly like a machine you ran install.sh on.
#
# Build (the format is chosen at build time, see Makefile / README):
Expand All @@ -28,7 +28,7 @@

{
imports = [
../../nixos/disko-standard.nix # 1 GB ESP + ZFS root pool single-disk layout
../../installer/bootstrap/disko-standard.nix # 1 GB ESP + ZFS root pool single-disk layout
../../nixos/_images/box-turnkey.nix # shared turn-key config (login + Coder bootstrap)
../../nixos/_images/base/disk.nix # bundles each disk image with its .sha256 (diskoImagesDir)
]
Expand Down
2 changes: 1 addition & 1 deletion hosts/_appliance-iso/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# nix build .#nixosConfigurations._appliance-iso.config.system.build.isoImage
#
# Unlike normal install hosts (hosts/<hostname>/), this host does NOT
# import nixos/disko-standard.nix, hardware-configuration.nix, or facter.json:
# import installer/bootstrap/disko-standard.nix, hardware-configuration.nix, or facter.json:
# the appliance root filesystem is the squashfs + tmpfs overlay provided by
# nixos/_images/appliance/iso.nix. All of the appliance-ISO wiring lives there.
#
Expand Down
2 changes: 1 addition & 1 deletion hosts/coder-thinkcentre/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# commit the result.
#
# Disk layout: this host stays on UUID-based mounts from hardware-configuration.nix.
# It does NOT import nixos/disko-standard.nix because doing so would conflict
# It does NOT import installer/bootstrap/disko-standard.nix because doing so would conflict
# with the existing fileSystems entries. Fresh installs of new hosts use
# disko-standard.nix and skip hardware-configuration.nix entirely.

Expand Down
Loading
Loading