Deploy on NixOS
The flake ships NixOS modules for the coordinator and the microVM sandbox adapter, plus packages:
| Flake output | What it is |
|---|---|
nixosModules.default (alias nixosModules.ngit-ci) | Coordinator service module (services.ngit-ci.instances.<name>) |
nixosModules.adapter-microvm | MicroVM sandbox adapter service module (services.ngit-ci-adapter-microvm) |
packages.<system>.default | The ngit-ci coordinator binary |
packages.<system>.adapter-microvm | The ngit-ci-adapter-microvm binary |
packages.<linux-system>.static | Statically linked ngit-ci binary |
packages.<linux-system>.adapter-microvm-static | Statically linked ngit-ci-adapter-microvm binary |
packages.x86_64-linux.microvm-guest-template | Nix-built qcow2 guest template for the adapter |
Add the flake input:
nix
inputs.ngit-ci.url =
"git+https://gitnostr.com/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-ci.git";Coordinator with the embedded act runner
nix
{
imports = [ inputs.ngit-ci.nixosModules.default ];
services.ngit-ci.instances.production = {
enable = true;
repos = [
"npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit#NGIT"
];
# Per-repo secrets, one runtime secret file per value (agenix/sops-nix
# paths, never Nix-store paths), wired to systemd LoadCredential.
# Alternatively secretsEnvironmentFile points at an EnvironmentFile of
# NGIT_CI_SECRET_NGIT__NAME=value lines; credentials win on conflicts.
# See configuration.md#per-repo-secrets.
repoSecrets.NGIT.API_TOKEN = "/run/agenix/ngit-api-token";
# Optional: accept encrypted Repository Secret Updates from maintainers
# on these inbox relays (empty disables Nostr secret intake). See
# configuration.md#receiving-secrets-over-nostr.
# nostrSecretRelays = [ "wss://secret-inbox.example" ];
# Optional: runtime secret file holding an established `nbunksec` used to
# seal Nostr-received values for repos whose maintainers bound no bunker
# of their own; wired as the `operator_bunker` systemd credential. See
# configuration.md#bunker-sealed-secret-values.
# operatorBunkerFile = "/run/agenix/ngit-ci-operator-nbunksec";
# Optional: pin the coordinator identity to an existing runtime secret
# file. Leave unset for a generated-and-persisted
# /var/lib/ngit-ci-production/.coordinator.nsec.
# coordinatorNsecFile = "/run/agenix/ngit-ci-coordinator-nsec";
# Optional: upload full job logs and workflow artifacts to Blossom.
blossomServers = [ "https://blossom.example" ];
# Operator-dictated per-job container spec (see "Job container
# resource limits" in docs/configuration.md); workflows cannot
# override it under the default actJobContainerPolicy = "admin".
actContainerOptions = "--memory=4g --memory-swap=4g --cpus=2 --pids-limit=2048";
# git and act are on the service PATH by default, and the module
# enables the host Docker daemon for act job containers
# (enableDockerIntegration).
};
}Each entry under services.ngit-ci.instances.<name> is a complete, independent coordinator — its own Nostr identity, repo set, and job queue — running as a separate hardened systemd service (ngit-ci-<name>) with its own state directory (/var/lib/ngit-ci-<name>, holding the generated .coordinator.nsec identity key, processed-ids state, queued-job checkpoint, and work dir). If you set coordinatorNsecFile, the module wires it as a systemd credential named coordinator_nsec; keep that file out of the Nix store. An invalid or empty configured key fails startup rather than causing ngit-ci to generate a new identity. If you leave coordinatorNsecFile unset, the module's persistent StateDirectory makes the generated .coordinator.nsec survive restarts and upgrades. Nostr secret intake is off until nostrSecretRelays names at least one inbox relay, which then enables it for every repository admitted by repos. The accepted values are kept encrypted under the instance's event cache directory in the same state directory. operatorBunkerFile is the NixOS form of NGIT_CI_OPERATOR_BUNKER: like coordinatorNsecFile it must be a runtime secret file rather than a Nix-store path, must contain an established nbunksec and never a one-time bunker:// URL, and reaches the service as the operator_bunker credential. Configure multiple instances to watch different repo sets under different identities, not to run more jobs in parallel — job concurrency within a coordinator is maxConcurrentJobs. Module options mirror the configuration reference; see nix/module.nix for the full option set and descriptions.
The generated unit's stop timeout covers one job timeout, the post-run Blossom budget, and shutdown overhead. On SIGTERM, queued starts freeze immediately, active jobs finish concurrently, and the remaining FIFO is checkpointed before systemd starts the upgraded coordinator. A socket-adapter coordinator also orders itself after ngit-ci-adapter-microvm.service, which reverses on stop so the adapter remains available until active microVM jobs finish.
Using Podman instead of Docker
Hosts that deliberately avoid the Docker daemon (membership of the docker group is root-equivalent) can point the service at a Podman socket instead. Set enableDockerIntegration = false so the module does not enable virtualisation.docker or add the docker supplementary group:
nix
{
virtualisation.podman.enable = true;
# Docker-compatible API socket at /run/podman/podman.sock (root podman,
# gated by the "podman" group).
virtualisation.podman.dockerSocket.enable = true;
services.ngit-ci.instances.production = {
enable = true;
repos = [ "npub1.../ngit" ];
enableDockerIntegration = false;
dockerHost = "unix:///run/podman/podman.sock";
};
# Grant the service access to the podman socket.
systemd.services.ngit-ci-production.serviceConfig.SupplementaryGroups =
[ "podman" ];
}A rootless (per-user) Podman socket at unix:///run/user/<uid>/podman/podman.sock also works — see testing.md for an ad hoc setup — but a system service then needs that user's socket running and accessible, so the system-level dockerSocket variant is the simpler default. If neither enableDockerIntegration nor dockerHost is configured, the module emits an eval-time warning because act will have no daemon to talk to.
Coordinator + microVM sandbox adapter
For VM-per-job isolation (see microvm-adapter.md), deploy both modules on a host with /dev/kvm:
nix
{
imports = [
inputs.ngit-ci.nixosModules.default
inputs.ngit-ci.nixosModules.adapter-microvm
];
services.ngit-ci-adapter-microvm = {
enable = true;
template =
"${inputs.ngit-ci.packages.x86_64-linux.microvm-guest-template}/template.qcow2";
maxJobs = 1;
vcpus = 2;
memoryMiB = 4096;
};
services.ngit-ci.instances.production = {
enable = true;
repos = [ "npub1.../ngit" ];
runner = "socket-adapter";
adapterSocket = "/run/ngit-ci-adapter-microvm/adapter.sock";
# No container daemon needed on the coordinator host: act runs inside
# the job VMs against the guest's own dockerd.
enableDockerIntegration = false;
};
}The adapter module grants the service /dev/kvm access and group-shares the socket with the coordinator (both default to the ngit-ci group). No other KVM configuration is needed: the only host prerequisite is that /dev/kvm exists, i.e. hardware virtualization is enabled in firmware (or nested virtualization if the host is itself a VM). Pair one coordinator instance with one adapter, with the coordinator's maxConcurrentJobs at or below the adapter's maxJobs (see Sandbox adapters).
Note that referencing the template package makes nixos-rebuild build it, which requires KVM on whatever machine performs the build; alternatively build it once (nix build .#microvm-guest-template) and set template to a plain path. See Building the guest template.