Skip to content

Configuration settings

Three rules run through all of it.

Configuration is a file and environment variables; policy is a bundle. Anything that decides who may do what lives in the policy bundle. Anything that decides how this process runs is configuration. Nothing crosses that line: there is no setting that grants access, and no policy rule that sets a port.

Layers, lowest to highest: defaults, file, environment. The environment wins because that is what an orchestrator injects, and because an operator debugging a host expects NIT_LOG_LEVEL=debug nitd to work whatever the file says.

A malformed setting stops the process. A bad duration, a short signing key, a misspelled key: the binary refuses to start and says which setting is wrong and where. Silently falling back to a default nobody chose is how a deployment ends up running something nobody intended.

Terminal window
nitctl config init # a fully commented starter file, mode 600
nitctl config path # which file would be read
nitctl config show # every effective value, and where it came from

Looked for in this order, first match wins:

  1. -config <path> on nitd or nit-worker
  2. $NIT_CONFIG
  3. ./nit.yaml, ./nit.yml
  4. $XDG_CONFIG_HOME/nit/nit.yaml
  5. $HOME/.config/nit/nit.yaml
  6. /etc/nit/nit.yaml, /etc/nit/nit.yml

Working directory first so a developer can run a checkout without touching the system; /etc/nit last so a package’s file is the fallback rather than the override.

A file named explicitly with -config or $NIT_CONFIG that does not exist is an error, not a fallback: an operator who named a file meant that file.

/etc/nit/nit.yaml
server:
addr: ":8080"
admin_groups: [platform]
cors_origins: []
event_max_wait: 30s
database:
url_file: /run/secrets/nit-database-url
policy:
dir: /etc/nit/policy
reload: 30s
storage:
blob_dir: /var/lib/nit/blobs
work_dir: /var/lib/nit/work
mirror_budget_bytes: 21474836480
max_patch_bytes: 104857600
pull_ttl: 24h
queue:
lease_duration: 60s
max_attempts: 3
poll: 1s
reap_every: 30s
security:
sync_key_file: /etc/nit/sync.key
forge:
token_file: /run/secrets/nit-forge-token
log:
level: info

A misspelled key is refused, naming the field:

nitd: configuration file /etc/nit/nit.yaml: yaml: unmarshal errors:
line 2: field adr not found in type bootstrap.fileServer

nitd and nit-worker read the same configuration, on purpose. Two processes that disagree about the database, the bundle or the signing key fail in ways that are very hard to diagnose.

File keyVariable
database.urlNIT_DATABASE_URLDatabase DSN — see Which database
security.sync_keyNIT_SYNC_KEYSync token signing key, 32 bytes minimum
policy.dirNIT_POLICY_DIRDirectory holding the policy bundle

The DSN selects the engine. There is no setting that names it, and one that matches neither shape is refused at start-up rather than guessed at.

EngineDSN shape
PostgreSQL 13+postgres://nit:secret@db:5432/nit?sslmode=require
MySQL 8.0.16+nit:secret@tcp(db:3306)/nit?tls=true
MariaDB 10.6+nit:secret@tcp(db:3306)/nit?tls=true

MySQL and MariaDB share one driver and one schema; nothing distinguishes them in configuration.

Everything the application does behaves identically across the three — not as a statement of intent, but because one conformance suite runs against all of them in CI.

TRUNCATE requires DROP. Withholding it is what stops the audit trail being erased in one word:

CREATE USER 'nit'@'%' IDENTIFIED BY 'secret';
GRANT SELECT, INSERT, UPDATE, DELETE ON nit.* TO 'nit'@'%';

Migrations need more, and are run by a different account on purpose — a deployment step an operator takes, never something the server does to itself:

GRANT ALL PRIVILEGES ON nit.* TO 'nit_migrate'@'%';

Every secret has an inline form and a _file twin. Prefer the file.

SecretFile keyVariable
Database URLdatabase.url_fileNIT_DATABASE_URL_FILE
Signing keysecurity.sync_key_fileNIT_SYNC_KEY_FILE
Forge tokenforge.token_fileNIT_FORGE_TOKEN_FILE

Docker secrets, Kubernetes secrets and systemd’s LoadCredential all deliver a secret as a file. Reading it directly means the value never appears in docker inspect, in a crash dump, or in a configuration file something else might back up. Trailing whitespace is trimmed, so the newline every mount adds does not become part of your signing key.

Setting both forms of the same secret in the file is an error rather than a precedence question nobody should have to remember. Across layers, the file indirection wins: a deployment that mounted a secret meant it.

A configuration file carrying an inline secret must be mode 600:

nitd: configuration file /etc/nit/nit.yaml is mode 0644 and contains secrets;
run: chmod 600 /etc/nit/nit.yaml — or move the secrets to sync_key_file, url_file and token_file

Refusing is deliberately louder than warning: a warning in a start-up log is a warning nobody sees. A file using only the _file indirections holds no secret and needs no special mode.

Terminal window
openssl rand -base64 32

A sync token is a client’s claim about which upstream commit its patch was computed against, and the server applies that patch on top of whatever the token names. The signature is what stops a client naming a base of its choosing.

There is deliberately no default and no generated fallback. A key generated at start-up would differ between replicas and across restarts, silently invalidating every client’s token — which, to a developer, looks like their workspace mysteriously demanding a full resynchronization.

Generate it once, share it across every replica, treat it like a database password. Rotating it invalidates every sync token in existence; every workspace needs one nit pull to recover.

File keyVariableDefault
storage.blob_dirNIT_BLOB_DIR./var/blobsPatch payloads
storage.work_dirNIT_WORK_DIR./var/workA worker’s git mirrors and task worktrees
storage.mirror_budget_bytesNIT_MIRROR_BUDGET_BYTES21474836480 (20 GiB)Disk the mirrors may occupy before the least recently used are evicted; 0 disables eviction

blob_dir must be shared between nitd and every worker. nitd writes the authorized patch there and the worker reads it back; two processes with two separate directories produce missing_patch on every push. On one host that means the same path; across hosts, a shared volume.

work_dir is scratch, local to each worker, never backed up, and belongs to one worker process — two workers sharing one would race on the same mirror.

It holds a bare mirror per repository, kept between tasks, plus one worktree per concurrent task. Only the worktrees free their disk on their own, so size the volume as mirror_budget_bytes plus the largest repository × concurrency. See Running workers.

File keyVariableDefault
log.levelNIT_LOG_LEVELinfodebug, info, warn, error
storage.max_patch_bytesNIT_MAX_PATCH_BYTES104857600Ceiling on a patch, compressed and decompressed
storage.pull_ttlNIT_PULL_TTL24hHow long a generated pull patch stays fetchable
policy.reloadNIT_POLICY_RELOAD30sHow often the bundle is reread
queue.lease_durationNIT_LEASE_DURATION60sHow long a worker may hold a task without a heartbeat
queue.max_attemptsNIT_MAX_ATTEMPTS3Retries before a task fails for good
queue.pollNIT_QUEUE_POLL1sHow often an idle worker asks for work
queue.reap_everyNIT_REAP_EVERY30sHow often abandoned tasks return to the queue

Durations accept Go syntax: 90s, 5m, 2h.

max_patch_bytes is checked on both the compressed and the decompressed size, because a decompression bomb is small until it is not.

lease_duration is the setting most likely to need changing — see Running workers.

File keyVariableDefault
server.addrNIT_ADDR:8080Listen address
server.admin_groupsNIT_ADMIN_GROUPS(empty)Groups allowed to read the operations API
server.cors_originsNIT_CORS_ORIGINS(empty)Browser origins allowed to call the API
server.event_max_waitNIT_EVENT_MAX_WAIT30sHow long a long poll is held open

The two lists are YAML sequences in the file and comma-separated in the environment.

Empty means the operations API is reachable by nobody. Non-members get 404, not 403.

It is server configuration rather than a policy rule on purpose: the console is the tool for diagnosing a broken bundle, and putting the permission to use it inside the bundle would make that tool depend on the thing it exists to debug.

Only needed when the console is served from a different origin than the API. Serve them from the same host — as the console’s nginx does — and leave this empty.

There is no wildcard. The API is bearer-authenticated, so * would let any page a developer happens to visit call it with their credentials.

server:
cors_origins: ["http://localhost:4200"] # development only

The CLI holds a long poll open while it waits for a task; the server answers after this long even if nothing changed, and the client reconnects. Shorter means more reconnections. Longer risks an intermediate proxy closing the connection first — behind a load balancer with a 30-second idle timeout, set this below it.

File keyVariableDefault
forge.tokenNIT_FORGE_TOKEN(empty)Credential nit pushes with, over HTTPS
git.ssh_commandNIT_GIT_SSH_COMMAND(empty)Passed to every git invocation as GIT_SSH_COMMAND
git:
ssh_command: >-
ssh -i /run/secrets/nit-ssh-key
-o IdentitiesOnly=yes
-o UserKnownHostsFile=/etc/nit/ssh/known_hosts
-o StrictHostKeyChecking=yes
-o BatchMode=yes

Leave forge.token unset for an SSH remote: the key does that job, and an unused secret is one more thing to rotate.

git.ssh_command is a passthrough, not an abstraction. There is deliberately no ssh_key setting — a key path would cover only the simplest case, and agents, ProxyJump, per-host keys and non-standard ports would still be git’s business. It exists so a deployment can keep everything in one file and read it back with nitctl config show, not so nit manages keys.

Empty leaves the inherited environment untouched, so a host already configured through ~/.ssh/config keeps working. A configured value overrides an inherited GIT_SSH_COMMAND: a setting that silently did nothing on a host that exports one would be worse than no setting at all.

FlagDefault
-queuespush,pullWhich task kinds this worker takes
-concurrency1Runners in this process
-namehostnameIdentifier recorded on leases and in logs
-config(the search order)Configuration file
FlagDefault
-config(the search order)Configuration file

Everything else is a setting, so that a container’s command line does not become a second place to look.

Terminal window
nitctl config path
nitctl config show
file: /etc/nit/nit.yaml
SETTING FROM VALUE
addr file 127.0.0.1:8080
database.url file postgres://postgres:***@localhost:5432/nit
log.level env DEBUG
policy.reload default 30s
queue.lease_duration file 5m0s
security.sync_key file (set)
server.admin_groups file platform

The FROM column is the point of the command: it answers why is this setting what it is? Secrets are never printed — only whether they are set and which layer supplied them.

Run it on every host, including inside the containers. A worker with a different blob_dir from nitd is the single most common misconfiguration, and this is what shows it.