aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--modules/home/dev-tools.nix25
-rw-r--r--modules/nixos/common.nix6
-rw-r--r--openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/.openspec.yaml2
-rw-r--r--openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/design.md40
-rw-r--r--openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/proposal.md26
-rw-r--r--openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/specs/container-runtime/spec.md19
-rw-r--r--openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/specs/developer-environment/spec.md12
-rw-r--r--openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/tasks.md14
-rw-r--r--openspec/specs/container-runtime/spec.md21
-rw-r--r--openspec/specs/developer-environment/spec.md8
10 files changed, 160 insertions, 13 deletions
diff --git a/modules/home/dev-tools.nix b/modules/home/dev-tools.nix
index 9533ef5..001a78b 100644
--- a/modules/home/dev-tools.nix
+++ b/modules/home/dev-tools.nix
@@ -1,14 +1,17 @@
1{ pkgs, lib, ... }: 1{ pkgs, lib, ... }:
2{ 2{
3 home.packages = (with pkgs; [ 3 home.packages =
4 gh 4 (with pkgs; [
5 tmux 5 gh
6 htop 6 tmux
7 yq-go 7 htop
8 docker 8 yq-go
9 docker-compose 9 docker
10 kubectl 10 docker-compose
11 awscli2 11 kubectl
12 bitwarden-cli 12 awscli2
13 ]) ++ lib.optional (lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.claude-code) pkgs.claude-code; 13 bitwarden-cli
14 uv
15 ])
16 ++ lib.optional (lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.claude-code) pkgs.claude-code;
14} 17}
diff --git a/modules/nixos/common.nix b/modules/nixos/common.nix
index a219850..10f48c6 100644
--- a/modules/nixos/common.nix
+++ b/modules/nixos/common.nix
@@ -68,4 +68,10 @@
68 nssmdns4 = true; 68 nssmdns4 = true;
69 openFirewall = true; 69 openFirewall = true;
70 }; 70 };
71
72 # Rootless: runs as the user's own systemd user service, not as root, and
73 # has no docker-group privilege-escalation path. setSocketVariable exports
74 # DOCKER_HOST automatically so the docker CLI (dev-tools.nix) just works.
75 virtualisation.docker.rootless.enable = true;
76 virtualisation.docker.rootless.setSocketVariable = true;
71} 77}
diff --git a/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/.openspec.yaml b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/.openspec.yaml
new file mode 100644
index 0000000..75289e4
--- /dev/null
+++ b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/.openspec.yaml
@@ -0,0 +1,2 @@
1schema: spec-driven
2created: 2026-09-24
diff --git a/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/design.md b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/design.md
new file mode 100644
index 0000000..6d25717
--- /dev/null
+++ b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/design.md
@@ -0,0 +1,40 @@
1## Context
2
3See proposal.md - Why. This covers the Docker daemon decisions; `uv` is a plain CLI-package addition with nothing else to decide.
4
5Confirmed by reading the pinned `nixos-unstable` nixpkgs' `nixos/modules/virtualisation/docker-rootless.nix`:
6- `virtualisation.docker.rootless.enable = true;` runs `dockerd-rootless` as a **user** systemd service (`systemd.user.services.docker`, `wantedBy = [ "default.target" ]`), gated by `ConditionUser = "!root"` — it does not run as a system-level root daemon and does not use the `docker` group at all (that's a rootful-mode concept only).
7- `virtualisation.docker.rootless.setSocketVariable = true;` exports `DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock` for the user automatically, so the already-installed `docker` CLI (from `modules/home/dev-tools.nix`) finds the daemon with no per-user shell config.
8- Rootless mode needs `newuidmap`/`newgidmap` (from `pkgs.shadow`, via `/run/wrappers`) and subuid/subgid ranges for the user; confirmed in `nixos/modules/config/users-groups.nix` that any `isNormalUser` account (which `tom` already is, in `modules/nixos/common.nix`) gets `autoSubUidGidRange = mkDefault true` for free — no extra config needed for `tom` specifically.
9
10## Goals / Non-Goals
11
12**Goals:**
13- `docker`/`docker-compose` (already provisioned via `modules/home/dev-tools.nix`) work out of the box on any NixOS host from this flake, with no manual daemon setup or `DOCKER_HOST` export.
14- No root-owned daemon process, and no `docker`-group privilege-escalation path (the well-known rootful-Docker security concern, since group membership in `docker` is root-equivalent).
15
16**Non-Goals:**
17- Rootful Docker, or a `docker` group — explicitly not used, per rootless choice.
18- Managing Docker on the standalone (non-NixOS) `tom@linux`/`tom@darwin` home-manager profiles — those keep relying on an externally-managed runtime (Docker Desktop/OrbStack), unchanged from today.
19- Any Kubernetes/cloud runtime — `kubectl`/`awscli2` remain explicitly client-only, per the narrowed `developer-environment` requirement.
20- TLS/remote Docker API exposure, custom daemon settings (registries, storage drivers, etc.) — only the daemon itself is turned on, with defaults.
21
22## Decisions
23
24**Decision: rootless over rootful Docker.**
25- User's explicit choice. Alternatives considered: rootful (`virtualisation.docker.enable = true;` + `users.users.tom.extraGroups = [ "docker" ]`) is the more common/simpler setup and has slightly broader compatibility (e.g. binding privileged ports below 1024, some volume-permission edge cases behave more predictably), but membership in the `docker` group is a well-documented root-equivalent privilege escalation (a container can bind-mount `/` and modify the host as root). Rootless avoids that entirely at the cost of those edge-case compatibility gaps.
26
27**Decision: unconditional on every NixOS host, added inline to `modules/nixos/common.nix`.**
28- User's explicit choice, matching the existing pattern for GNOME/SSH/printing/avahi in that same file — no per-host opt-in mechanism exists elsewhere in this repo for system services, so introducing one just for Docker would be an inconsistent one-off. Added inline rather than as a new `modules/nixos/docker.nix` file, consistent with how every other unconditional system-level concern in this repo (GNOME, SSH, dconf, pipewire, printing) already lives directly in `common.nix` rather than being split out.
29
30**Decision: rely on nixpkgs' default `autoSubUidGidRange` rather than setting explicit `subUidRanges`/`subGidRanges` on `tom`.**
31- Simpler, and confirmed correct by reading the actual default-assignment logic in `users-groups.nix` (`isNormalUser` + no explicit ranges set → `autoSubUidGidRange` defaults to `true`). Setting an explicit range would only add a pin to maintain for no behavioral benefit.
32
33**Decision: narrow the existing `developer-environment` "client-only" requirement rather than leaving it as-is and letting the new `container-runtime` capability silently contradict it.**
34- Alternatives considered: leaving the old requirement's blanket wording untouched and only adding `container-runtime` was rejected — a future reader of "Container and cloud tools are client-only" would have no indication Docker is now an exception, and `openspec validate`'s spec/behavior contract would be misleading. Explicitly naming Docker as the carved-out exception keeps the two capabilities' specs mutually consistent.
35
36## Risks / Trade-offs
37
38- [Rootless Docker has real compatibility gaps vs. rootful: binding ports <1024 needs extra config, some third-party images assume a rootful daemon, performance can be slightly lower due to the userspace network stack (slirp4netns/VPNKit-style)] → Accepted trade-off per the user's explicit security-over-compatibility choice; not mitigated further in this change.
39- [The user systemd service only starts on user login (`wantedBy = [ "default.target" ]` at the *user* manager level), not at boot before any login] → Expected and consistent with rootless Docker's design (it's inherently tied to a logged-in user's session); not a regression from "no daemon at all" today.
40- [`docker` package is already installed twice in concept — once via `modules/home/dev-tools.nix`'s `pkgs.docker` and again via `virtualisation.docker.rootless.package` which nixpkgs adds to `environment.systemPackages`] → Harmless: both resolve to the same nixpkgs `docker` derivation and Nix store paths dedupe; no functional conflict, just two provisioning paths converging on the same package (home-manager for cross-platform coverage, NixOS module as an inherent side effect of enabling the service).
diff --git a/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/proposal.md b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/proposal.md
new file mode 100644
index 0000000..74391f1
--- /dev/null
+++ b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/proposal.md
@@ -0,0 +1,26 @@
1## Why
2
3Tom wants `uv` (the Python package/project manager) available as a CLI tool, and — after clarifying the request — wants NixOS hosts to actually run a Docker daemon rather than just the client CLI that's already provisioned, so `docker run`/`docker build` work without a separately-managed runtime.
4
5## What Changes
6
7- Add `pkgs.uv` to `modules/home/dev-tools.nix`'s CLI package list. Verified against the flake's pinned `nixos-unstable` nixpkgs: dual Apache-2.0/MIT licensed (free), broad platform support including `aarch64-darwin`/`x86_64-linux`/`aarch64-linux`. No spec change needed — `developer-environment`'s existing "Developer CLI tools are provisioned via the shared home profile" requirement already covers adding another evidenced CLI tool to the list, the same reasoning previously used for `bitwarden-cli` and `claude-code`.
8- Enable a **rootless** Docker daemon (`virtualisation.docker.rootless.enable = true;` plus `virtualisation.docker.rootless.setSocketVariable = true;`) unconditionally on every NixOS host, in `modules/nixos/common.nix`, alongside the other unconditional host-level services (GNOME, SSH, printing). Rootless runs the daemon as a per-user systemd user service under `tom`'s own UID rather than as root — no `docker`-group privilege-escalation path, unlike the standard rootful setup. `setSocketVariable` makes the already-installed `docker` CLI (from `dev-tools.nix`) find the rootless socket automatically via `$DOCKER_HOST`, with no extra per-user config.
9- **This reverses part of the existing "Container and cloud tools are client-only" policy** for Docker specifically: `developer-environment`'s spec currently states container/cloud tools SHALL NOT manage the underlying runtime. Docker becomes an explicit, called-out exception; `kubectl` and `awscli2` remain client-only as before (no Kubernetes cluster or cloud account is being provisioned).
10- **BREAKING**: none — additive only. Existing `docker`/`docker-compose` CLI usage against an externally-managed runtime (e.g. Docker Desktop/OrbStack on macOS) is unaffected; this only changes NixOS hosts, where no runtime was being managed before.
11
12## Capabilities
13
14### New Capabilities
15- `container-runtime`: provides a working, unprivileged (rootless) Docker container runtime on every NixOS host built from this flake, so the already-provisioned `docker`/`docker-compose` CLI tools have something to talk to without a separately-managed daemon.
16
17### Modified Capabilities
18- `developer-environment`: narrow the "Container and cloud tools are client-only" requirement to explicitly exclude Docker, which is now backed by the `container-runtime` capability on NixOS hosts; `kubectl` and `awscli2` remain covered by the original client-only rule.
19
20## Impact
21
22- **Affected code**: `modules/home/dev-tools.nix` (add `pkgs.uv`), `modules/nixos/common.nix` (add `virtualisation.docker.rootless.enable`/`setSocketVariable`).
23- **Not affected**: `lib/unfree-allowlist.nix` (uv is free-licensed; Docker's daemon package in nixpkgs is Apache-2.0), `modules/home/desktop-apps.nix`.
24- **Dependencies**: nixpkgs' `uv` and `docker` (already used) packages, both already present in the pinned `nixos-unstable` input; no new flake inputs.
25- **Platforms**: `uv` is provisioned cross-platform like the rest of `dev-tools.nix`. The Docker daemon change only applies to NixOS hosts (`modules/nixos/common.nix`); it has no effect on the standalone `tom@linux`/`tom@darwin` home-manager profiles, which continue to rely on whatever runtime is already present on that machine (e.g. Docker Desktop/OrbStack on macOS), exactly as documented today.
26- **Known limitation, out of scope**: no Docker daemon TLS/remote-access configuration, no Docker Compose v1-vs-v2 plugin wiring changes, and no `nix-ld`/extra container-toolchain packages beyond what's already provisioned — this only turns on the daemon itself.
diff --git a/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/specs/container-runtime/spec.md b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/specs/container-runtime/spec.md
new file mode 100644
index 0000000..e0565c6
--- /dev/null
+++ b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/specs/container-runtime/spec.md
@@ -0,0 +1,19 @@
1## Purpose
2
3Provides a working, unprivileged Docker container runtime on every NixOS host built from this flake, so the already-provisioned Docker CLI tooling has a daemon to talk to without a separately-managed installation step.
4
5## ADDED Requirements
6
7### Requirement: A rootless Docker daemon runs on every NixOS host
8Every NixOS host built from this flake's shared system module SHALL run a Docker daemon in rootless mode (as the user's own account, not as root), without requiring per-host opt-in.
9
10#### Scenario: Daemon available after boot
11- **WHEN** a NixOS host built from this flake's shared system module finishes booting and the user logs in
12- **THEN** a Docker daemon is running under that user's own account, not as root
13
14### Requirement: The Docker CLI connects to the rootless daemon without manual configuration
15A user on a NixOS host built from this flake's shared system module SHALL be able to run Docker CLI commands (e.g. `docker run`, `docker build`) against the rootless daemon without manually setting `DOCKER_HOST` or other connection configuration.
16
17#### Scenario: Docker command works out of the box
18- **WHEN** a user runs a `docker` CLI command on a NixOS host built from this flake's shared system module
19- **THEN** the command reaches the rootless daemon and executes, without the user having exported `DOCKER_HOST` or written any Docker connection config themselves
diff --git a/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/specs/developer-environment/spec.md b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/specs/developer-environment/spec.md
new file mode 100644
index 0000000..8a76849
--- /dev/null
+++ b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/specs/developer-environment/spec.md
@@ -0,0 +1,12 @@
1## MODIFIED Requirements
2
3### Requirement: Container and cloud tools are client-only
4Container and cloud-related packages provisioned by this capability SHALL provide client/CLI functionality only; they SHALL NOT attempt to provide or manage the underlying runtime or service (e.g. a Kubernetes cluster or cloud account), with one exception: Docker's runtime is managed on NixOS hosts by the separate `container-runtime` capability. Outside of NixOS hosts, and for every other tool (`kubectl`, `awscli2`), no corresponding runtime or account is installed or managed.
5
6#### Scenario: No runtime is installed or managed
7- **WHEN** a container or cloud CLI tool other than Docker is provisioned by the shared home profile
8- **THEN** no corresponding daemon, virtual machine, or account configuration is installed or managed by this capability — the tool simply connects to whatever runtime or account is already configured on the system
9
10#### Scenario: Docker's runtime is the documented exception on NixOS
11- **WHEN** the Docker CLI is provisioned by the shared home profile on a NixOS host built from this flake's shared system module
12- **THEN** a Docker runtime is available via the separate `container-runtime` capability, rather than being left entirely unmanaged
diff --git a/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/tasks.md b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/tasks.md
new file mode 100644
index 0000000..14b6dfc
--- /dev/null
+++ b/openspec/changes/archive/2026-09-24-add-uv-and-docker-runtime/tasks.md
@@ -0,0 +1,14 @@
1## 1. uv CLI tool
2
3- [x] 1.1 Add `pkgs.uv` to the package list in `modules/home/dev-tools.nix` and verify `nix eval '.#homeConfigurations."tom@linux".config.home.packages'` and `..."tom@darwin"...` both include it.
4
5## 2. Rootless Docker daemon
6
7- [x] 2.1 Add `virtualisation.docker.rootless.enable = true;` and `virtualisation.docker.rootless.setSocketVariable = true;` to `modules/nixos/common.nix`, alongside the other unconditional host-level services.
8- [x] 2.2 Build a NixOS host's system closure with `hosts/test-vm` visible via the `path:` flake-ref scheme (it's git-untracked): `nix build 'path:/home/tom/desktop-flake#nixosConfigurations.test-vm.config.system.build.toplevel'`, and verify it succeeds.
9- [x] 2.3 Inspect the built closure's `systemd.user.services.docker` unit (e.g. via the closure's `etc/systemd/user/docker.service` or an equivalent generated unit file) and verify it runs `dockerd-rootless` with `ConditionUser=!root`, confirming the daemon is wired as a rootless per-user service rather than a root daemon.
10- [x] 2.4 On a booted host, after logging in as `tom`, verify `docker run --rm hello-world` succeeds with no manual `DOCKER_HOST` export or other setup — this needs an interactive login session and can't be exercised from this sandbox.
11
12## 3. Spec sync
13
14- [x] 3.1 Verify `openspec validate --change add-uv-and-docker-runtime --strict` passes once the above are implemented, confirming both delta specs match the shipped behavior.
diff --git a/openspec/specs/container-runtime/spec.md b/openspec/specs/container-runtime/spec.md
new file mode 100644
index 0000000..0b07f92
--- /dev/null
+++ b/openspec/specs/container-runtime/spec.md
@@ -0,0 +1,21 @@
1# container-runtime Specification
2
3## Purpose
4
5Provides a working, unprivileged Docker container runtime on every NixOS host built from this flake, so the already-provisioned Docker CLI tooling has a daemon to talk to without a separately-managed installation step.
6
7## Requirements
8
9### Requirement: A rootless Docker daemon runs on every NixOS host
10Every NixOS host built from this flake's shared system module SHALL run a Docker daemon in rootless mode (as the user's own account, not as root), without requiring per-host opt-in.
11
12#### Scenario: Daemon available after boot
13- **WHEN** a NixOS host built from this flake's shared system module finishes booting and the user logs in
14- **THEN** a Docker daemon is running under that user's own account, not as root
15
16### Requirement: The Docker CLI connects to the rootless daemon without manual configuration
17A user on a NixOS host built from this flake's shared system module SHALL be able to run Docker CLI commands (e.g. `docker run`, `docker build`) against the rootless daemon without manually setting `DOCKER_HOST` or other connection configuration.
18
19#### Scenario: Docker command works out of the box
20- **WHEN** a user runs a `docker` CLI command on a NixOS host built from this flake's shared system module
21- **THEN** the command reaches the rootless daemon and executes, without the user having exported `DOCKER_HOST` or written any Docker connection config themselves
diff --git a/openspec/specs/developer-environment/spec.md b/openspec/specs/developer-environment/spec.md
index 374f098..bcdc3e4 100644
--- a/openspec/specs/developer-environment/spec.md
+++ b/openspec/specs/developer-environment/spec.md
@@ -12,12 +12,16 @@ The shared home-manager profile SHALL include the user's evidenced most-used CLI
12- **THEN** the provisioned developer CLI tools are available on `PATH` without any additional manual installation step 12- **THEN** the provisioned developer CLI tools are available on `PATH` without any additional manual installation step
13 13
14### Requirement: Container and cloud tools are client-only 14### Requirement: Container and cloud tools are client-only
15Container and cloud-related packages provisioned by this capability SHALL provide client/CLI functionality only; they SHALL NOT attempt to provide or manage the underlying runtime or service (e.g. a container engine or cloud account). 15Container and cloud-related packages provisioned by this capability SHALL provide client/CLI functionality only; they SHALL NOT attempt to provide or manage the underlying runtime or service (e.g. a Kubernetes cluster or cloud account), with one exception: Docker's runtime is managed on NixOS hosts by the separate `container-runtime` capability. Outside of NixOS hosts, and for every other tool (`kubectl`, `awscli2`), no corresponding runtime or account is installed or managed.
16 16
17#### Scenario: No runtime is installed or managed 17#### Scenario: No runtime is installed or managed
18- **WHEN** a container or cloud CLI tool is provisioned by the shared home profile 18- **WHEN** a container or cloud CLI tool other than Docker is provisioned by the shared home profile
19- **THEN** no corresponding daemon, virtual machine, or account configuration is installed or managed by this capability — the tool simply connects to whatever runtime or account is already configured on the system 19- **THEN** no corresponding daemon, virtual machine, or account configuration is installed or managed by this capability — the tool simply connects to whatever runtime or account is already configured on the system
20 20
21#### Scenario: Docker's runtime is the documented exception on NixOS
22- **WHEN** the Docker CLI is provisioned by the shared home profile on a NixOS host built from this flake's shared system module
23- **THEN** a Docker runtime is available via the separate `container-runtime` capability, rather than being left entirely unmanaged
24
21### Requirement: Shell and prompt configuration defers to the user's personal dotfiles 25### Requirement: Shell and prompt configuration defers to the user's personal dotfiles
22Where the user's personal dotfiles repository already owns a piece of shell or prompt configuration, the shared home profile SHALL provide only the underlying package, not a competing managed configuration for that same file. 26Where the user's personal dotfiles repository already owns a piece of shell or prompt configuration, the shared home profile SHALL provide only the underlying package, not a competing managed configuration for that same file.
23 27