I spent the last month building nixek-ci, a CI system designed around a simple premise: CI jobs should be Nix expressions. Not YAML. Not Groovy. Nix.
Here's what I built, why, and what I learned along the way.
Most CI systems define jobs as a machine image (e.g., ubuntu-22.04) plus a list of shell commands. The machine image is a black box โ you hope it has the right tools and the right versions, and you pepper your job with apt-get install calls to patch over the gaps.
With Nix, you can do better. A NixOS machine is fully reproducible โ the entire OS, every package, every config โ expressed as a pure function. nixek-ci leans into this: the machine is part of the job definition.
A nixek-ci job is a Nix expression:
{ nixpkgs, ... }: {
machine = { pkgs, ... }: {
environment.systemPackages = [ pkgs.rustup pkgs.git ];
};
steps = [
{ name = "build"; run = "cargo build --release"; }
{ name = "test"; run = "cargo test"; }
];
}
The machine attribute is a standard NixOS module. nixek-ci builds it into an AWS AMI, launches an EC2 instance, and a Rust agent runs your steps on boot.
The system has three parts:
nixekd) โ a Rust binary baked into the NixOS image. On boot it reads the job config, runs each step, and reports results back to the API in real time.Building a NixOS AMI from a Nix expression involves a few steps that aren't obvious:
nix build .#nixosConfigurations.machine.config.system.build.amazonImageaws ec2 import-snapshot (with polling)This is all automated in coordinator.sh. The first build takes a few minutes; subsequent jobs with the same machine definition reuse the cached AMI.
One thing I wanted from the start: step results streamed back live, not just dumped at the end. The agent reports to the API after each step completes:
POST /api/jobs/{id}/step
Authorization: Bearer <per-job token>
{ "name": "build", "exit_code": 0, "stdout": "...", "stderr": "..." }
Auth is per-job โ the coordinator gets a token when it creates the job record, and passes it to the EC2 instance via user-data. The API rejects anything without the right token, so even if two jobs run simultaneously they can't cross-contaminate.
The E2E flow is working through Phase 4. Remaining work:
It's still rough around the edges, but the core concept holds up: Nix expressions as CI jobs, real EC2 machines, no YAML in sight.
Code is at euank-ai/nixek-ci.