NixOS Module
The eka-ci flake provides a NixOS module at nixosModules.daemon that exposes the
service under services.eka-ci. The module uses the RFC-42 "settings" pattern: most
configuration is freeform TOML that gets serialized to ekaci.toml, with common fields
typed for validation and auto-generated documentation.
Quick Start
{
inputs.eka-ci.url = "github:ekala-project/eka-ci";
outputs = { self, nixpkgs, eka-ci, ... }: {
nixosConfigurations.example = nixpkgs.lib.nixosSystem {
modules = [
eka-ci.nixosModules.daemon
{
services.eka-ci = {
enable = true;
environmentFile = "/run/secrets/eka-ci.env";
settings = {
github_apps = [{
id = "main";
credentials.systemd-credential.name = "github-app-key";
}];
security.allow_insecure_webhooks = false;
};
};
}
];
};
};
}
The service runs as a systemd DynamicUser by default, stores state under
/var/lib/eka-ci, and listens on 127.0.0.1:3030.
Top-Level Options
services.eka-ci.enable
Type: boolean
Default: false
Enable the EkaCI server.
services.eka-ci.package
Type: package
Default: pkgs.eka-ci
Package providing the eka_ci_server binary.
services.eka-ci.user / services.eka-ci.group
Type: string
Default: "eka-ci"
User and group the service runs as when dynamicUser = false. Ignored when
dynamicUser = true.
services.eka-ci.dynamicUser
Type: boolean
Default: true
Use systemd's DynamicUser= to run the service under an ephemeral user/group. Recommended
unless you need a stable UID for filesystem permissions on shared storage.
services.eka-ci.openFirewall
Type: boolean
Default: false
Open settings.web.port in the system firewall.
services.eka-ci.environmentFile
Type: null or path
Default: null
Path to a file passed to systemd as EnvironmentFile=. Use this to provide secrets such
as WEBHOOK_SECRET, GITHUB_OAUTH_CLIENT_SECRET, JWT_SECRET, VAULT_TOKEN,
GITEA_TOKEN, GITLAB_TOKEN, AWS keys, and any environment variables referenced from
settings.caches.*.credentials.env.vars.
The file is read by systemd at start time and never enters the Nix store.
Example (/run/secrets/eka-ci.env):
WEBHOOK_SECRET=your-webhook-secret
GITHUB_OAUTH_CLIENT_SECRET=...
JWT_SECRET=...
VAULT_TOKEN=s.abc123...
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
# For Gitea integration (single instance)
GITEA_TOKEN=your-gitea-token
GITEA_DOMAIN=gitea.example.com
# For GitLab integration (single instance)
GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxx
GITLAB_DOMAIN=gitlab.com
services.eka-ci.credentials
Type: attribute set of path
Default: {}
Map of credential name to file path, wired through systemd's LoadCredential=. Each entry
becomes available inside the unit at $CREDENTIALS_DIRECTORY/<name> and can be referenced
from ekaci.toml via the systemd-credential credential source.
Example:
services.eka-ci = {
credentials.github-app-key = "/run/secrets/github-app.json";
settings.github_apps = [{
id = "main";
credentials.systemd-credential.name = "github-app-key";
}];
};
Pairs naturally with sops-nix, agenix, or systemd-creds.
services.eka-ci.extraEnvironment
Type: attribute set of string
Default: {}
Additional Environment= entries passed to the systemd unit.
Example:
services.eka-ci.extraEnvironment = {
RUST_LOG = "eka_ci_server::scheduler=debug,info";
};
Settings Options
The services.eka-ci.settings submodule is freeform: any key not explicitly listed below is
still accepted and serialized to ekaci.toml as-is. Typed options provide validation and
documentation for the most common fields.
settings.db_path
Type: null or path
Default: null
SQLite database path. When null the server falls back to $XDG_DATA_HOME/ekaci/sqlite.db,
which under this module resolves to /var/lib/eka-ci/ekaci/sqlite.db.
settings.logs_dir
Type: null or path
Default: null
Directory where build logs are stored. When null the server falls back to
$XDG_DATA_HOME/ekaci/build-logs.
settings.require_approval
Type: boolean
Default: false
Require maintainer approval before building PRs from external contributors.
settings.merge_queue_require_approval
Type: boolean
Default: false
Require approval before building entries pulled from the GitHub merge queue.
settings.build_no_output_timeout_seconds
Type: integer between 30 and 86400
Default: 1200
Number of seconds with no build output after which a build is considered hung.
settings.build_max_duration_seconds
Type: integer between 60 and 604800
Default: 14400
Hard upper bound, in seconds, on total build wall-clock time.
settings.graph_lru_capacity
Type: positive integer
Default: 100000
Capacity of the in-memory derivation-graph LRU cache, in nodes. See LRU Cache Tuning for sizing guidance.
settings.default_merge_method
Type: one of "merge", "squash", "rebase"
Default: "squash"
Default merge method used by the @eka-ci merge PR comment command.
settings.web
Type: submodule
HTTP server settings.
web.address(string, default"127.0.0.1"): IPv4 address the HTTP server binds to.web.port(port, default3030): TCP port the HTTP server binds to.web.bundle_path(null or path, defaultnull): Optional path to a pre-built web UI bundle.web.allowed_origins(list of string, default[]): CORS allow-list. Each entry must be a fully-qualifiedhttp://orhttps://origin with no path, query, fragment, or*wildcard. An empty list rejects all cross-origin requests.
settings.unix
Type: submodule
Unix domain socket settings used by the CLI client.
unix.socket_path(null or path, defaultnull): Unix domain socket the CLI client connects to. Whennullthe server falls back to$XDG_RUNTIME_DIR/ekaci.socket, which under this module resolves to/run/eka-ci/ekaci.socket.
settings.oauth
Type: submodule
OAuth settings for the (optional) web UI.
oauth.client_id(null or string, defaultnull): GitHub OAuth client ID. May also be supplied via theGITHUB_OAUTH_CLIENT_IDenvironment variable (preferred — seeenvironmentFile).oauth.client_secret(null or string, defaultnull): GitHub OAuth client secret. Avoid setting this in Nix — values here end up in the world-readable Nix store. UseenvironmentFileto supplyGITHUB_OAUTH_CLIENT_SECRETinstead.oauth.redirect_url(null or string, defaultnull): OAuth callback URL. Defaults tohttp://{web.address}:{web.port}/github/auth/callbackwhen unset.oauth.jwt_secret(null or string, defaultnull): JWT signing secret. Avoid setting this in Nix. ProvideJWT_SECRETviaenvironmentFile. When omitted entirely, the server generates an ephemeral 256-bit secret on each start (sessions invalidate across restarts).
settings.security
Type: submodule
Security-related settings.
security.max_hook_timeout_seconds(integer between 1 and 86400, default300): Maximum wall-clock time, in seconds, that any post-build hook is allowed to run.security.audit_hooks(boolean, defaulttrue): Emit structured audit log records every time a hook runs.security.webhook_secret(null or string, defaultnull): Webhook HMAC secret used for all platforms (GitHub, GitLab, and Gitea). Avoid setting this in Nix. ProvideWEBHOOK_SECRETviaenvironmentFile. The server refuses to start if no webhook secret is available unlessallow_insecure_webhooksistrue.security.allow_insecure_webhooks(boolean, defaultfalse): Allow the server to start without a webhook secret. Intended for local development only; never enable in production.security.allow_private_cache_hosts(boolean, defaultfalse): Allow cache destinations whose DNS resolves to private/loopback addresses. Disables built-in SSRF protection; only enable in trusted, isolated networks.
settings.caches
Type: list of submodule
Default: []
List of binary caches the server may push to.
Each cache entry has the following fields:
id(string, required): Cache identifier referenced from.eka-ci/config.json.cache_type(one of "nix-copy", "cachix", "attic", required): Backend type for this cache.destination(string, required): Destination URL passed to the chosen backend. Validated for SSRF unlesssettings.security.allow_private_cache_hostsis set.credentials(freeform, required): Credential source. See Credential Sources below.permissions(submodule, default allows all): Repository/branch access control.allow_all(boolean, defaulttrue): Whentrue, ignoresallowed_reposandallowed_branchesand grants access to every repository and branch.allowed_repos(list of string, default[]): Glob patterns ofowner/repostrings that are permitted to use this entry.allowed_branches(list of string, default[]): Glob patterns of branch names permitted to use this entry.
Example:
settings.caches = [{
id = "production-s3";
cache_type = "nix-copy";
destination = "s3://my-bucket/nix-cache?region=us-east-1";
credentials.env.vars = [ "AWS_ACCESS_KEY_ID" "AWS_SECRET_ACCESS_KEY" ];
permissions = {
allow_all = false;
allowed_repos = [ "myorg/*" ];
allowed_branches = [ "main" "release/*" ];
};
}];
settings.github_apps
Type: list of submodule
Default: []
List of GitHub Apps the server authenticates as.
Each GitHub App entry has the following fields:
id(string, required): GitHub App identifier.credentials(freeform, required): Credential source. See Credential Sources below.permissions(submodule, default allows all): Same structure assettings.caches.*.permissions.
Example:
settings.github_apps = [{
id = "main";
credentials.file.path = "/run/secrets/github-app.json";
permissions = {
allow_all = false;
allowed_repos = [ "myorg/*" ];
};
}];
settings.gitea_instances
Type: list of submodule
Default: []
List of Gitea instances the server integrates with. Each instance requires a domain and access token. Supports both Gitea.com and self-hosted instances.
Each Gitea instance entry has the following fields:
domain(string, required): Gitea instance domain (without protocol), e.g.,"gitea.example.com".token(null or string, defaultnull): Gitea access token. Avoid setting this in Nix — useenvironmentFileto supplyGITEA_TOKENinstead (for single instance setups).
Example:
settings.gitea_instances = [
{
domain = "gitea.example.com";
token = null; # Provided via environmentFile
}
{
domain = "code.company.net";
token = null; # Provided via environmentFile
}
];
For single-instance setups, you can use environment variables:
# In environmentFile
GITEA_TOKEN=your-gitea-access-token
GITEA_DOMAIN=gitea.example.com
settings.gitlab_instances
Type: list of submodule
Default: []
List of GitLab instances the server integrates with. Each instance requires a domain and project access token. Supports both GitLab.com and self-hosted instances.
Each GitLab instance entry has the following fields:
domain(string, required): GitLab instance domain (without protocol), e.g.,"gitlab.com"or"gitlab.example.com".token(null or string, defaultnull): GitLab project access token (starts withglpat-). Avoid setting this in Nix — useenvironmentFileto supplyGITLAB_TOKENinstead (for single instance setups).
Example:
settings.gitlab_instances = [
{
domain = "gitlab.com";
token = null; # Provided via environmentFile
}
{
domain = "gitlab.enterprise.com";
token = null; # Provided via environmentFile
}
];
For single-instance setups, you can use environment variables:
# In environmentFile
GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxx
GITLAB_DOMAIN=gitlab.com
Credential Sources
Both settings.caches.*.credentials and settings.github_apps.*.credentials accept one of
ten credential source variants. The field is freeform (not exhaustively typed) so all variants
serialize correctly. Choose the one that matches your secret-management setup:
1. Environment variables
credentials.env.vars = [ "AWS_ACCESS_KEY_ID" "AWS_SECRET_ACCESS_KEY" ];
The server reads the listed environment variables at runtime. Provide them via
environmentFile.
2. File
credentials.file.path = "/etc/eka-ci/creds.json";
The server reads a JSON or KEY=VALUE file at the given path.
3. AWS profile
credentials.aws-profile.profile = "production";
The server reads credentials from ~/.aws/credentials using the named profile.
4. Cachix token
credentials.cachix-token.env_var = "CACHIX_AUTH_TOKEN";
The server reads a Cachix auth token from the named environment variable.
5. HashiCorp Vault
credentials.vault = {
address = "https://vault.example.com:8200";
secret_path = "secret/data/eka-ci/s3-cache";
token_env = "VAULT_TOKEN"; # optional, defaults to "VAULT_TOKEN"
namespace = "production"; # optional
};
The server authenticates to Vault using the token from token_env and reads the secret at
secret_path.
6. AWS Secrets Manager
credentials.aws-secrets-manager = {
secret_name = "eka-ci/s3-credentials";
region = "us-east-1"; # optional, falls back to AWS_REGION env var
};
The server uses AWS SDK credential resolution (environment, instance metadata, profiles) to authenticate to AWS Secrets Manager and reads the named secret.
7. systemd credential
credentials.systemd-credential.name = "github-app-key";
The server reads the credential from $CREDENTIALS_DIRECTORY/<name>. Pair this with the
top-level services.eka-ci.credentials option:
services.eka-ci.credentials.github-app-key = "/run/secrets/github-app.json";
8. Instance metadata
credentials = "instance-metadata";
The server retrieves credentials from EC2/GCP/Azure instance metadata. No configuration needed.
9. GitHub App key file
credentials.github-app-key-file = {
app_id_env = "GITHUB_APP_ID";
key_file = "/etc/eka-ci/github-app.pem";
};
The server reads the GitHub App ID from the named environment variable and the PEM-encoded private key from the file.
10. None
credentials = "none";
No authentication. Only valid for public caches.
Systemd Hardening
The module applies aggressive systemd hardening by default:
DynamicUser = true(ephemeral user/group)ProtectSystem = "strict"(read-only/usr,/boot,/efi)ProtectHome = true(no access to/home,/root)PrivateTmp = true(isolated/tmp)PrivateDevices = true(empty/dev)NoNewPrivileges = true(no privilege escalation)ProtectKernelModules/Tunables/Logs = trueProtectControlGroups/Clock/Hostname = trueRestrictNamespaces/Realtime/SUIDSGID = trueRestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]LockPersonality = trueMemoryDenyWriteExecute = trueSystemCallArchitectures = "native"SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]- Empty
CapabilityBoundingSetandAmbientCapabilities UMask = "0077"
If you need to relax any of these, override systemd.services.eka-ci.serviceConfig in your
configuration.
Complete Example
{ config, ... }:
{
services.eka-ci = {
enable = true;
openFirewall = false; # Behind a reverse proxy
environmentFile = config.sops.secrets.eka-ci-env.path;
credentials = {
github-app-key = config.sops.secrets.github-app-json.path;
s3-creds = config.sops.secrets.s3-json.path;
};
extraEnvironment.RUST_LOG = "info";
settings = {
web = {
address = "127.0.0.1";
port = 3030;
allowed_origins = [ "https://ci.example.com" ];
};
graph_lru_capacity = 200000; # Large repo
default_merge_method = "squash";
security = {
audit_hooks = true;
allow_insecure_webhooks = false;
};
github_apps = [{
id = "main";
credentials.systemd-credential.name = "github-app-key";
permissions = {
allow_all = false;
allowed_repos = [ "myorg/*" ];
};
}];
gitea_instances = [{
domain = "gitea.example.com";
token = null; # Provided via environmentFile
}];
gitlab_instances = [{
domain = "gitlab.com";
token = null; # Provided via environmentFile
}];
caches = [
{
id = "s3-production";
cache_type = "nix-copy";
destination = "s3://my-bucket/nix-cache?region=us-east-1";
credentials.systemd-credential.name = "s3-creds";
permissions = {
allow_all = false;
allowed_repos = [ "myorg/production-*" ];
allowed_branches = [ "main" ];
};
}
{
id = "cachix-public";
cache_type = "cachix";
destination = "myorg";
credentials.cachix-token.env_var = "CACHIX_AUTH_TOKEN";
}
];
};
};
# Reverse proxy
services.nginx.virtualHosts."ci.example.com" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:3030";
proxyWebsockets = true;
};
};
}
See Also
- Module Reference — auto-generated complete option reference
- Server Configuration — detailed
ekaci.tomlreference - Configuring Caches — cache setup and credential management
- GitHub App Setup — creating and configuring GitHub Apps
- LRU Cache Tuning — sizing
graph_lru_capacity