diff --git a/coderd/templates/coder-cli/README.md b/coderd/templates/coder-cli/README.md new file mode 100644 index 0000000..783aad5 --- /dev/null +++ b/coderd/templates/coder-cli/README.md @@ -0,0 +1,56 @@ +--- +display_name: Coder CLI / Dogfood +description: Full-featured dev workspace running the Coder oss-dogfood image — docker CLI, terraform, gh, go, node, and more pre-installed. +icon: /icon/coder.svg +maintainer_github: coder +verified: false +tags: [docker, cli, dogfood] +--- + +# Coder CLI / Dogfood + +A general-purpose developer workspace built on the `codercom/oss-dogfood` image. Everything you'd reach for in a demo or workshop is already installed — no setup required. + +## What's included + +- **Coder CLI** — `coder` binary matching the server version +- **Docker CLI** — talks to the host Docker/Podman socket via `DOCKER_HOST` +- **Terraform** + **OpenTofu** +- **GitHub CLI** (`gh`) +- **Go**, **Node.js**, **Python 3** +- **git**, **curl**, **jq**, **make**, and the usual UNIX toolkit +- **code-server** — VS Code in the browser, pre-installed +- **Cursor** desktop app link + +## Images + +| Option | Image | Base OS | +|---|---|---| +| Ubuntu 22.04 (default) | `codercom/oss-dogfood:latest` | Ubuntu 22.04 | +| Ubuntu 26.04 | `codercom/oss-dogfood:26.04` | Ubuntu 26.04 | +| Nix (experimental) | `codercom/oss-dogfood-nix:latest` | NixOS | + +The image is immutable — rebuild the workspace to switch. + +## Parameters + +| Parameter | Description | +|---|---| +| Workspace image | Base image (immutable — rebuild to change) | +| CPU cores | 1–16 | +| Memory (GiB) | Configurable | +| Home disk (GiB) | 10–200 GiB, immutable | + +## How it works + +This template runs workspaces as Docker containers directly on the host (not via k3s). The container gets: + +- The host Docker/Podman socket bind-mounted as `/var/run/docker.sock` +- A persistent named volume for `/home/coder` +- `CODER_AGENT_TOKEN` and `CODER_AGENT_URL` injected via environment + +The Coder agent starts inside the container via the `init_script` entrypoint and reports back to the server. + +## Requirements + +Docker or rootless Podman must be running on the host. No k3s or sysbox needed — this template is the lightest option and works on any host in this repo. diff --git a/coderd/templates/k3s-sysbox/main.tf b/coderd/templates/k3s-sysbox/main.tf index 47e918b..1e0ccf9 100644 --- a/coderd/templates/k3s-sysbox/main.tf +++ b/coderd/templates/k3s-sysbox/main.tf @@ -44,7 +44,7 @@ locals { ws_name = lower(data.coder_workspace.me.name) prefix = "coder-${data.coder_workspace.me.id}" pod_name = "${local.prefix}-pod" - kubectl = "/nix/store/5bx4yn3kq9rgyaianxxldnr2ffr2k5fr-k3s-1.34.5+k3s1/bin/k3s kubectl --kubeconfig /etc/rancher/k3s/k3s.yaml" + kubectl = "/run/current-system/sw/bin/k3s kubectl --kubeconfig /etc/rancher/k3s/k3s.yaml" } # ── Parameters ──────────────────────────────────────────────────────────── @@ -385,8 +385,10 @@ resource "terraform_data" "workspace" { hostUsers = false restartPolicy = "OnFailure" hostname = "${local.owner}-${local.ws_name}" - hostAliases = [{ - }] + hostAliases = length(var.coder_lan_ip) > 0 ? [{ + ip = var.coder_lan_ip + hostnames = [var.coder_hostname] + }] : [] containers = [{ name = "workspace" image = data.coder_parameter.image.value diff --git a/configuration.nix b/configuration.nix index 1c88c8f..5acea3b 100644 --- a/configuration.nix +++ b/configuration.nix @@ -360,7 +360,7 @@ in if [ -z "''${CODER_ADMIN_EMAIL:-}" ]; then echo "CODER_ADMIN_EMAIL not set, skipping bootstrap." - echo "Complete the first-run wizard at http://$(hostname -s).local:3000" + echo "Complete the first-run wizard at http://$(${pkgs.nettools}/bin/hostname -s).local:3000" exit 0 fi @@ -515,8 +515,13 @@ in # 8. Mint a fresh long-lived session token using the admin creds from local.nix 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}"}" | ${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) + 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}\"}" \ + | ${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) echo "$LONG_TOKEN" | ${pkgs.coreutils}/bin/tee /etc/coder/session-token > /dev/null echo "--- session token written" @@ -554,6 +559,7 @@ in echo " This file is auto-populated by coder-init-admin.service on first boot." else mkdir -p "$STATE_DIR" + chown coder:coder "$STATE_DIR" 2>/dev/null || true 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") @@ -688,7 +694,10 @@ EOF echo "coder-workspace-reaper: checking for workspaces stopped before $(${pkgs.coreutils}/bin/date -d @$CUTOFF --iso-8601=seconds)" - WORKSPACES=$(${pkgs.curl}/bin/curl -sf -H "Coder-Session-Token: $TOKEN" "$CODER_LOCAL/api/v2/workspaces?limit=100&filterQuery=status:stopped" | ${pkgs.jq}/bin/jq -r '.workspaces[] | .id + " " + .name + " " + .last_used_at') + WORKSPACES=$(${pkgs.curl}/bin/curl -sf \ + -H "Coder-Session-Token: $TOKEN" \ + "$CODER_LOCAL/api/v2/workspaces?limit=100&filterQuery=status:stopped" \ + | ${pkgs.jq}/bin/jq -r '.workspaces[] | .id + " " + .name + " " + .last_used_at') if [ -z "$WORKSPACES" ]; then echo "coder-workspace-reaper: no stopped workspaces found" @@ -700,7 +709,10 @@ EOF LAST_EPOCH=$(${pkgs.coreutils}/bin/date -d "$last_used" +%s 2>/dev/null || echo 0) if [ "$LAST_EPOCH" -lt "$CUTOFF" ]; then echo "coder-workspace-reaper: deleting $name ($id) — last used $last_used" - ${pkgs.curl}/bin/curl -sf -X DELETE -H "Coder-Session-Token: $TOKEN" "$CODER_LOCAL/api/v2/workspaces/$id" || echo "coder-workspace-reaper: WARNING — delete failed for $name" + ${pkgs.curl}/bin/curl -sf -X DELETE \ + -H "Coder-Session-Token: $TOKEN" \ + "$CODER_LOCAL/api/v2/workspaces/$id" || \ + echo "coder-workspace-reaper: WARNING — delete failed for $name" else echo "coder-workspace-reaper: keeping $name — stopped recently" fi diff --git a/hosts/incus-vm/README.md b/hosts/incus-vm/README.md new file mode 100644 index 0000000..f404d60 --- /dev/null +++ b/hosts/incus-vm/README.md @@ -0,0 +1,219 @@ +# hosts/incus-vm — Running box on a headless host + +## What box does + +Box turns a NixOS machine into a **self-contained Coder deployment**. After +`nixos-rebuild switch`, the machine runs: + +- **Coder server** — full control plane, accessible over a `*.try.coder.app` + tunnel or a configured external URL +- **PostgreSQL** — Coder's database, managed by the box flake +- **k3s + sysbox** (optional) — single-node Kubernetes cluster where Coder + provisions workspaces as pods; sysbox-runc gives each pod its own Docker daemon + without privileged mode +- **template-sync** — an activation hook that runs `terraform apply` on + `coderd/` at every `nixos-rebuild switch`, keeping Coder templates in sync with + the repo automatically + +The result: SSH or `coder ssh` into the machine, and you have a working Coder +instance. Workspace pods run on the same node via k3s. No separate infrastructure +required. + +--- + +## What this directory provides + +`incus-vm.nix` adapts the base box config for a headless VM or server: + +- Imports the upstream `incus-virtual-machine.nix` profile (QEMU guest agents, + virtio drivers) — skip this for bare-metal +- `systemd-networkd` DHCP on `enp5s0` (the virtio NIC Incus assigns to VMs on + both x86_64 and aarch64) +- Disables the KDE / PipeWire / printing / Avahi stack — a headless host only + needs Coder + PostgreSQL + +`default.nix` is the per-host entrypoint that imports `incus-vm.nix`, your +`local.nix` secrets file, and the runtime files from `/etc/nixos/` (see below). + +The same pattern works for bare-metal machines (ThinkStation, etc.) — just skip +`incus-vm.nix` or replace it with your own hardware module. + +--- + +## Relationship to the incus-nixos registry template + +[`registry.coder.com/templates/bpmct/incus-nixos`](https://registry.coder.com/templates/bpmct/incus-nixos) +is a **separate, unrelated** Coder workspace template. It provisions a plain NixOS +VM as a Coder *workspace* (something you SSH into to do work), using +`nixos-rebuild switch` via `incus exec`. It writes its own minimal +`configuration.nix` at first boot and has nothing to do with this flake. + +| | `bpmct/incus-nixos` registry template | `hosts/incus-vm/` in this repo | +|---|---|---| +| What is it? | A Coder workspace template | A box host config | +| End result | A NixOS VM you work inside | A NixOS machine that *runs* Coder | +| Uses box flake? | No | Yes | +| Runs Coder server? | No — runs coder-agent | Yes — full Coder + PostgreSQL + k3s | +| Who runs it? | Coder Terraform provisioner | You, manually, on the host | + +You can use both together: run the `incus-nixos` template from a box host to spin +up NixOS workspaces, while the host itself is set up with this flake. + +--- + +## Manual setup: fresh NixOS host → box + +These steps work for an Incus VM provisioned by the `incus-nixos` template (or +any other NixOS VM) **and** for bare-metal machines. + +> **Note:** A stock NixOS image does not have `git` installed. Use +> `nix-shell -p git` to get it temporarily for the clone step, or add it to the +> system environment first. + +### 1. Clone the repo + +```sh +nix-shell -p git --run "git clone https://github.com/coder/box /etc/nixos-repo" +``` + +### 2. Create the host directory + +The flake auto-discovers hosts by folder name — the folder name must match +`hostname -s`: + +```sh +HOSTNAME=$(hostname -s) +mkdir -p /etc/nixos-repo/hosts/$HOSTNAME + +# For an Incus VM: +cp /etc/nixos-repo/hosts/incus-vm/default.nix \ + /etc/nixos-repo/hosts/$HOSTNAME/default.nix +cp /etc/nixos-repo/hosts/incus-vm/incus-vm.nix \ + /etc/nixos-repo/hosts/$HOSTNAME/incus-vm.nix + +# For bare-metal — write your own default.nix or copy from another host. +# See hosts/qemu-arm64/ for an example layout. + +# Stage the files — the flake's builtins.readDir only sees tracked files. +git -C /etc/nixos-repo add hosts/$HOSTNAME/ +``` + +> **`/etc/nixos/coder.nix`:** The copied `default.nix` does **not** import this +> file. It only exists on VMs that are *also* running as a coder-agent workspace +> (i.e. the `incus-nixos` template writes it). On a pure box host it won't be +> present, and you don't need it. + +### 3. Set architecture and enable k3s + +Edit `hosts/$HOSTNAME/default.nix`. + +**aarch64 VMs:** The flake defaults to `x86_64-linux`. If your VM is ARM +(e.g. running on Apple Silicon or an ARM server), add this or the build will +evaluate for the wrong architecture: + +```nix +nixpkgs.hostPlatform = "aarch64-linux"; +``` + +Then enable k3s (required for workspace provisioning): + +```nix +# sysbox-runc — each workspace pod gets its own Docker daemon (no privileged mode) +services.coder-nixos.k3s-sysbox.enable = true; +``` + +Or for the lighter rootless-Podman variant: + +```nix +services.coder-nixos.k3s.enable = true; +``` + +> Only enable one. `k3s-sysbox` is required for the `k3s-sysbox` workspace +> template; `k3s` works with `k3s-podman` and `k3s-dev`. + +### 4. Create local.nix + +`local.nix` holds per-host secrets (admin credentials, LAN IP, SSH keys). It is +gitignored and must be created manually: + +```sh +cp /etc/nixos-repo/local.nix.example \ + /etc/nixos-repo/hosts/$HOSTNAME/local.nix + +# Mark it so the flake's builtins.readDir can see it without committing it. +git -C /etc/nixos-repo add --intent-to-add -f hosts/$HOSTNAME/local.nix +``` + +Edit `hosts/$HOSTNAME/local.nix` and at minimum set: + +```nix +services.coder-nixos.lanIp = "192.168.x.x"; # VM's primary IP + +systemd.services.coder.environment = { + CODER_ADMIN_EMAIL = "you@example.com"; + CODER_ADMIN_USERNAME = "admin"; + CODER_ADMIN_PASSWORD = "changeme"; +}; +``` + +These credentials are read by `coder-init-admin.service` on first boot to +automatically create the admin user and mint a long-lived session token for +template-sync. **Without this, templates won't be pushed on the first +`nixos-rebuild switch`.** + +### 5. Write the runtime hostname file + +This file lives outside the flake so it doesn't need to be committed. On an +Incus VM provisioned by the `incus-nixos` template, `/etc/nixos/incus.nix` is +already written at first boot. For a fresh VM or bare-metal, create it manually: + +```sh +cat > /etc/nixos/incus.nix << 'EOF' +{ lib, ... }: +{ + networking.hostName = lib.mkForce "your-hostname"; +} +EOF +``` + +### 6. Apply + +```sh +nixos-rebuild switch --flake /etc/nixos-repo#$(hostname -s) --impure +``` + +`--impure` is required because `/etc/nixos/incus.nix` lives outside the flake +tree. This will build and activate: Coder server, PostgreSQL, k3s, sysbox, +and all supporting services. + +On first boot, `coder-init-admin.service` runs automatically after Coder starts: +creates the admin user, mints a long-lived session token to +`/etc/coder/session-token`, and pushes all workspace templates (`k3s-sysbox`, +`k3s-podman`, `k3s-dev`, `coder-cli`) via Terraform. Check progress with: + +```sh +journalctl -u coder-init-admin -f +``` + +Once complete, the tunnel URL is in `/etc/motd`: + +```sh +cat /etc/motd +``` + +**Fallback (no local.nix credentials):** If `CODER_ADMIN_EMAIL` was left empty, +`coder-init-admin` is skipped. Complete setup via the first-run wizard instead: + +```sh +# Find the tunnel URL: +journalctl -u coder --no-pager | grep "View the Web UI" +``` + +Open that URL in a browser, create the admin user, then log in with the CLI: + +```sh +CODER_URL=http://localhost:3000 coder login http://localhost:3000 +``` + +Once logged in, run `sudo nixos-rebuild switch` again to push templates via +`template-sync`. diff --git a/hosts/incus-vm/default.nix b/hosts/incus-vm/default.nix new file mode 100644 index 0000000..28e57a6 --- /dev/null +++ b/hosts/incus-vm/default.nix @@ -0,0 +1,31 @@ +# Template host config for a box host running inside an Incus VM. +# +# Copy this to hosts//default.nix and incus-vm.nix to the same +# folder, then follow hosts/incus-vm/README.md. +# +# --impure is required because /etc/nixos/incus.nix is a runtime file +# outside the flake tree. +# +# To enable k3s add one of these below: +# services.coder-nixos.k3s-sysbox.enable = true; # sysbox-runc (Docker per workspace) +# services.coder-nixos.k3s.enable = true; # rootless Podman variant + +{ lib, ... }: + +{ + imports = [ + ./incus-vm.nix # QEMU guest agents, networkd DHCP, no desktop stack + ./local.nix # per-host secrets: admin creds, LAN IP, SSH keys + /etc/nixos/incus.nix # hostname — written by incus-virtual-machine init + # /etc/nixos/coder.nix # only needed if this VM is also a coder-agent workspace + ]; + + # Uncomment for aarch64 VMs (Apple Silicon, ARM servers, etc.). + # The flake defaults to x86_64-linux; without this the build evaluates + # for the wrong architecture and will fail or produce a broken system. + # nixpkgs.hostPlatform = "aarch64-linux"; + + services.coder-nixos.k3s-sysbox.enable = true; + + system.stateVersion = "25.11"; +} diff --git a/hosts/incus-vm/incus-vm.nix b/hosts/incus-vm/incus-vm.nix new file mode 100644 index 0000000..36348d0 --- /dev/null +++ b/hosts/incus-vm/incus-vm.nix @@ -0,0 +1,60 @@ +# NixOS module for Incus VM guests. +# +# Import this in hosts//default.nix for any machine that lives +# inside an Incus virtual machine (as opposed to bare-metal or LXC). +# +# What it does: +# - Imports the upstream incus-virtual-machine.nix profile (QEMU guest +# agents, virtio drivers, auto-resize, systemd-boot). +# - Switches networking to systemd-networkd + DHCP on enp5s0 (the +# virtio NIC Incus assigns to VMs on both x86_64 and aarch64). +# - Disables the full desktop stack (KDE, PipeWire, printing, Avahi) +# that configuration.nix enables by default — a headless VM only needs +# the Coder server + PostgreSQL. +# +# k3s / sysbox are NOT enabled here. Set: +# services.coder-nixos.k3s-sysbox.enable = true; +# in hosts//default.nix to enable the full box stack (Coder + +# PostgreSQL + k3s + sysbox) inside the VM. + +{ lib, modulesPath, ... }: + +{ + imports = [ + # Use path concatenation (not string interpolation) so this works in + # pure eval mode when building from a flake. + (modulesPath + "/virtualisation/incus-virtual-machine.nix") + ]; + + # Incus VMs get a virtio NIC named enp5s0 on both x86_64 and aarch64. + # Use systemd-networkd instead of dhcpcd (already disabled by + # incus-virtual-machine.nix, but be explicit). + networking = { + dhcpcd.enable = false; + useDHCP = false; + useHostResolvConf = false; + }; + + systemd.network = { + enable = true; + networks."50-enp5s0" = { + matchConfig.Name = "enp5s0"; + networkConfig = { + DHCP = "ipv4"; + IPv6AcceptRA = true; + }; + linkConfig.RequiredForOnline = "routable"; + }; + }; + + # The full desktop stack from configuration.nix is not needed in a VM. + # Use lib.mkForce to win against the lib.mkDefault values set there. + services.xserver.enable = lib.mkForce false; + services.displayManager.sddm.enable = lib.mkForce false; + services.desktopManager.plasma6.enable = lib.mkForce false; + services.pipewire.enable = lib.mkForce false; + services.pulseaudio.enable = lib.mkForce false; + security.rtkit.enable = lib.mkForce false; + services.printing.enable = lib.mkForce false; + services.avahi.enable = lib.mkForce false; +} diff --git a/nixos/k3s-sysbox.nix b/nixos/k3s-sysbox.nix index 65425a6..e30f2e2 100644 --- a/nixos/k3s-sysbox.nix +++ b/nixos/k3s-sysbox.nix @@ -229,7 +229,9 @@ in (pkgs.writeShellScript "k3s-wait-api-ready" '' echo "k3s-wait-api-ready: waiting for API server /readyz..." for i in $(seq 1 120); do - if ${pkgs.curl}/bin/curl -sf -o /dev/null --cacert /var/lib/rancher/k3s/server/tls/server-ca.crt https://127.0.0.1:6443/readyz 2>/dev/null; then + if ${pkgs.curl}/bin/curl -sf -o /dev/null \ + --cacert /var/lib/rancher/k3s/server/tls/server-ca.crt \ + https://127.0.0.1:6443/readyz 2>/dev/null; then echo "k3s-wait-api-ready: API ready after ''${i}s" exit 0 fi @@ -358,7 +360,9 @@ in users.groups.coder = lib.mkDefault {}; # ── Additional packages ─────────────────────────────────────────────── - environment.systemPackages = with pkgs; [ kubectl kubernetes-helm fuse fuse3 ]; + # rsync is required by sysbox-mgr preflight check; without it, + # sysbox-mgr exits immediately and pods stay stuck in ContainerCreating. + environment.systemPackages = with pkgs; [ kubectl kubernetes-helm fuse fuse3 rsync ]; # ── FUSE: allow non-root mounts (needed by sysbox-fs) ───────────────── programs.fuse.userAllowOther = true;