@@ -5,10 +5,10 @@ A Nix flake that builds an **OCI image** (via
55Apple's [`container`](https://github.com/apple/container) CLI on macOS.
66
77It's a plain, single-process image — no NixOS, no systemd — defined in
8−[`flake.nix`](./flake.nix). The default command is `fish` (with a configured
9−prompt), and the image ships a handful of dev/LLM tools: `git`, `jujutsu`,
10−`ripgrep`, `fd`, `jq`, `nodejs`, `python3`, a Rust + C toolchain,
11−`charmbracelet.crush`, `openssh`, and more.
8+[`flake.nix`](./flake.nix). `c start` drops you into a
9+[`herdr`](https://herdr.dev) session whose panes use your host login shell
10+(detected and bundled by `c init`). You choose what else it ships via a
11+per-project `container.nix`.
1212
1313## Prerequisites
1414
@@ -16,90 +16,102 @@ prompt), and the image ships a handful of dev/LLM tools: `git`, `jujutsu`,
1616- Apple's [`container`](https://github.com/apple/container) CLI installed, with
1717 services started: `container system start` (first run downloads a Linux
1818 kernel).
19−- A **Linux builder** — the image *contents* are Linux binaries, which can't be
20− built on macOS. Use a [remote builder][builders] or, on Apple Silicon, the
21− `nix-darwin` `linux-builder`. With one configured, `nix` dispatches Linux
22− builds to it automatically (most packages also come prebuilt from the binary
23− cache, so the builder is only needed for the uncached bits).
19+- A **Linux builder** *only if you use packages that aren't in the binary
20+ cache*. The image is assembled entirely on macOS — the root filesystem is just
21+ a host-built symlink tree over the **substituted** Linux store paths — so a
22+ fully-cached package set needs no builder. Compiling a Linux package that
23+ `cache.nixos.org` doesn't have does, though: use a [remote builder][builders]
24+ or, on Apple Silicon, the `nix-darwin` `linux-builder`.
2425
2526[builders]: https://nix.dev/manual/nix/latest/advanced-topics/distributed-builds
2627
27−## Steps
28+## Packages
29+
30+`init` builds the image from a **`container.nix`** in the current directory — an
31+attrset with a `packages` function (`{ pkgs, nur }` returning the package list;
32+`nur` is [NUR](https://github.com/nix-community/NUR), for packages outside
33+nixpkgs) and an optional `mounts` list. It is **required**; there is no default.
34+
35+```nix
36+# ./container.nix
37+{
38+ packages = { pkgs, nur }: with pkgs; [
39+ git
40+ go
41+ nur.repos.charmbracelet.crush
42+ ];
43+ # Extra "host:container" bind mounts, on top of $PWD → /workspace. A leading
44+ # ~/ in the host path expands to $HOME. Optional.
45+ mounts = [
46+ "~/data:/data"
47+ ];
48+}
49+```
2850
29−### 1. Build the image archive
51+Your **host login shell** (detected by `c init`, bash fallback), **coreutils**,
52+[**herdr**](https://herdr.dev) (the session `c start` launches), **Nix** itself
53+(with CA certs and `nix-command`/`flakes` enabled, single-user), and the ncurses
54+**terminfo** database are always included — no need to list them.
3055
31−`nix run` builds the Linux contents (on your Linux builder) and writes a tagged
32−OCI archive locally via `skopeo`. Pass the output path; the image is tagged
33−`nix-container:latest`.
56+Point at a different file with `-c`/`--config`:
3457
3558```sh
36−nix run . -- image.tar.gz
59+nix run github:ilyakooo0/nix-container -- init --config envs/ci.nix
3760```
3861
39−### 2. Load it into `container`
40−
41−```sh
42−container image load --input image.tar.gz
43−```
62+## Quick start
4463
45−### 3. Create and start the container
64+No clone needed — `nix run` drives everything. Run it from a project directory
65+that holds a `container.nix`; the container is named after that directory.
4666
4767```sh
48−container create --name nixos --ssh -it nix-container:latest
49−container start -ai nixos
68+# build the image, load it, and create the container (named after $PWD):
69+nix run github:ilyakooo0/nix-container -- init
70+
71+# start it and attach — drops you into a herdr session:
72+nix run github:ilyakooo0/nix-container -- start
5073```
5174
75+- `init` builds the OCI archive in a temp dir, loads it into `container`,
76+ deletes the temp file, then (re)creates the container. It mounts the current
77+ directory at `/workspace` (the container's working directory).
5278- `--ssh` forwards your host SSH agent socket into the container (so `git` over
53− SSH etc. works with your keys).
54−- `-it` / `-ai` give an interactive TTY; `start -ai` attaches to it.
55−
56−You land in `fish`. Exit the shell to stop the container.
57−
58−> **Shortcut:** the [`c`](./c) script bundles the workflow and names the
59−> container after the current directory (handy for per-project containers). It
60−> resolves the flake from its own location, so you can symlink it onto your
61−> `$PATH` and run it from any project:
62−>
63−> ```sh
64−> ./c init # build + load the image, then create the container
65−> ./c init -v $PWD:/work # add create-time mounts (set only at creation)
66−> ./c start # start and attach
67−> ```
68−>
69−> `c init` builds the archive in a temp dir, loads it, and deletes it — so no
70−> `image.tar.gz` is left lying around.
79+ SSH works with your keys).
80+- Add more mounts persistently via `mounts` in `container.nix` (above), or
81+ ad-hoc by passing them through to `create` (mounts can only be set at creation):
7182
72−## Re-running after a rebuild
83+ ```sh
84+ nix run github:ilyakooo0/nix-container -- init -v $HOME/data:/data
85+ ```
7386
74−`container` keeps the loaded image and the created container, so to pick up a
75−new build, remove the old ones first:
87+Working from a checkout, [`./c init`](./c) / `./c start` are equivalent — and
88+`c` can be symlinked onto your `$PATH` and run from any project.
7689
77−```sh
78−nix run . -- image.tar.gz # rebuild the archive
79−container rm nixos # remove the old container (stop it first if running)
80−container image rm nix-container:latest
81−container image load --input image.tar.gz
82−container create --name nixos --ssh -it nix-container:latest
83−container start -ai nixos
84−```
85−
86−## Other image destinations
90+## Re-running after a rebuild
8791
88−`nix run .#copyTo` is nix2container's generic skopeo wrapper
89−(`skopeo copy nix:<image> "$@"`) — the argument is any skopeo destination:
92+Just run `init` again — it reloads the image and replaces the existing container
93+(removing the old one first), so there's nothing to clean up:
9094
9195```sh
92−nix run .#copyTo -- docker://ghcr.io/me/nixos:latest # push to a registry
93−nix run .#copyTo -- docker-daemon:nix-container:latest # local Docker daemon
96+nix run github:ilyakooo0/nix-container -- init
9497```
9598
96−## Customize
99+## Customize further
100+
101+Fork or clone to change what isn't per-project — `config.cmd`/`env` or the image
102+name — in [`flake.nix`](./flake.nix). The flake also exposes build helpers:
97103
98−Edit the `buildImage` call in [`flake.nix`](./flake.nix):
104+- `lib.<system>.copyWith` → a skopeo copy app for a `{ pkgs, nur }: [ ... ]`
105+ function, for building/pushing the image anywhere skopeo can write (e.g. a
106+ registry):
99107
100−- Add packages to `copyToRoot.paths`.
101−- Change the default process via `config.cmd`.
102−- Add env vars via `config.env`.
108+ ```sh
109+ drv=$(nix build --no-link --print-out-paths --impure --expr \
110+ '(builtins.getFlake "github:ilyakooo0/nix-container").lib.${builtins.currentSystem}.copyWith (import ./container.nix).packages')
111+ $drv/bin/copy-to docker://ghcr.io/me/img:latest
112+ ```
113+- `lib.<system>.mkImage <name> <shell>` → the underlying OCI image (`copyWith`
114+ defaults these to `nix-container` / `bash`).
103115
104116The flake uses `flake-utils` over `{aarch64,x86_64}-{darwin,linux}`; the host
105117system maps to the matching Linux target automatically (an Intel Mac builds an
@@ -107,9 +119,12 @@ system maps to the matching Linux target automatically (an Intel Mac builds an
107119
108120## How it works
109121
110−- **Contents** (`copyToRoot`) are built from `pkgsLinux` → the Linux builder.
111−- **Assembly** (`nix2container` + the `skopeo` copy) runs on your Mac, so a
112− single `nix run` produces a loadable archive.
122+- **Contents** are Linux store paths — downloaded prebuilt from the binary cache
123+ (only built on a Linux builder if not cached).
124+- **Assembly** runs entirely on your Mac: `copyToRoot` is a *host-arch* `buildEnv`
125+ symlink tree over those Linux paths (symlinks are arch-neutral), and
126+ `nix2container` + `skopeo` package it — so for a cached set, `init` produces a
127+ loadable Linux image with no Linux build at all.
113128- Apple's `container` runs each container in its own lightweight Linux VM and
114129 consumes standard OCI images — no `--privileged` or systemd needed.
115130