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
56 changes: 56 additions & 0 deletions coderd/templates/coder-cli/README.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 5 additions & 3 deletions coderd/templates/k3s-sysbox/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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
Expand Down
22 changes: 17 additions & 5 deletions configuration.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down
219 changes: 219 additions & 0 deletions hosts/incus-vm/README.md
Original file line number Diff line number Diff line change
@@ -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`.
31 changes: 31 additions & 0 deletions hosts/incus-vm/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Template host config for a box host running inside an Incus VM.
#
# Copy this to hosts/<hostname>/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";
}
Loading