NIB backend — Go API server and agent orchestrator for infrastructure planning & execution
3.8K
API server and agent orchestrator for NIB (Neuro Infrastructure Builder) — a tool for planned infrastructure work. You describe a task; NIB decomposes it into stages, works each stage out into elementary steps (code changes, commands, clicks in a web console), derives the verification checks and the rollback plan, and then either executes the steps itself or hands the operator instructions detailed enough to follow without guessing.
This image is the engine of the stack. It runs the agent loop against an OpenAI-compatible LLM endpoint, keeps plans and chats in PostgreSQL, searches the knowledge base through pgvector, calls out to remote MCP servers for tools, and launches agent containers on the host Docker daemon or as Kubernetes Jobs.
It is one of four images: nib-backend (this one), nib-frontend
(the web UI), nib-kb (knowledge-base MCP server) and
nib-agent (the coding-agent runner).
Work happens in modes, and the backend implements all of them:
Around those sit the things that make the agent's output specific to your estate: rules (your accepted principles per infrastructure component, injected into detailed planning when the agent identifies the relevant systems), skills (markdown playbooks the agent loads on demand), variables and secrets (encrypted at rest, substituted into MCP server definitions only at call time) and the knowledge base.
The backend needs PostgreSQL with the pgvector extension. On its own:
docker run -d --name nib-backend \
-p 8080:8080 -p 9090:9090 \
-e DB_HOST=postgres -e DB_PORT=5432 \
-e DB_USER=nib -e DB_PASSWORD=nib -e DB_NAME=nib -e DB_SSLMODE=disable \
-e LLM_API_KEY="$LLM_API_KEY" \
-e SECRETS_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
-v nibdata:/app/data \
javdet/nib-backend:latest
The whole stack — Postgres, backend, frontend, knowledge base — comes up together with the Compose file in the repository:
git clone https://github.com/javdet/nib && cd nib
cp .env.example .env # set LLM_API_KEY
docker compose up -d # UI on http://localhost:8080
or with the bundled Helm chart:
helm install nib ./deploy/helm/nib -n nib --create-namespace \
--set secrets.llmApiKey="$LLM_API_KEY"
Database migrations from /app/migrations are applied on startup — no separate migration step.
Configuration is a config.yaml (baked into the image at /app/config.yaml, mount your own over
it) with environment variables taking precedence. That order is deliberate: Compose and Helm pass
METRICS_*, DB_* and SERVER_* as env, and a stale config.yaml left on the data volume must
not win over them.
| Variable | Default | Purpose |
|---|---|---|
SERVER_HOST / SERVER_PORT | 0.0.0.0 / 8080 | REST API listener |
DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME / DB_SSLMODE | — | PostgreSQL connection; the database needs pgvector |
MIGRATIONS_PATH | /app/migrations | SQL migrations applied at startup |
DATA_DIR | /app/data | Persistent state — see the volume layout below |
SEED_DIR | /app/seed | Defaults copied into DATA_DIR on first start |
LOG_LEVEL | info | debug logs full tool-call arguments and responses |
The backend talks to a single OpenAI-compatible endpoint for both chat and embeddings.
| Variable | Purpose |
|---|---|
LLM_API_KEY (fallback OPENAI_API_KEY) | API key |
LLM_BASE_URL | e.g. https://api.openai.com/v1, https://openrouter.ai/api/v1, https://generativelanguage.googleapis.com/v1beta/openai/ |
LLM_MODEL (fallback OPENAI_MODEL) | Model id as the provider spells it (OpenRouter uses provider/model slugs) |
LLM_API | responses (/v1/responses) or chat (/v1/chat/completions). Some models reject function tools on chat completions unless reasoning is off, so tools + reasoning needs responses; use chat for gateways whose responses support is absent or unverified |
LLM_REASONING_EFFORT | none, minimal, low, medium, high, xhigh |
LLM_PROVIDER | Provider preset / OpenRouter fallback behaviour |
OPENAI_EMBEDDING_MODEL | Embedding model for knowledge-base search. Must match kb_collections.embedding_model exactly — it is compared as a string |
Details, including the OpenRouter fallback, are in docs/llm-providers.md.
| Variable | Purpose |
|---|---|
DOCKER_HOST | Local executor — set to unix:///var/run/docker.sock and mount the socket |
EXECUTOR_LLM_API_KEY / EXECUTOR_LLM_MODEL | Credentials and model handed to the agent container (may differ from the orchestrator's) |
EXECUTOR_GIT_API_TOKEN | Token the agent uses to push branches and open PRs |
AGENT_WEBHOOK_TOKEN | Shared secret authenticating the agent's callback to /api/v1/agent-runner/webhook |
Executor type is chosen in Settings → Executor: Disabled (default, Execute action greyed
out on code steps), Local (Docker socket), or Remote → Kubernetes. For Kubernetes the backend
renders backend/internal/executor/templates/job.yaml.tmpl (override it by dropping your own at
{DATA_DIR}/job.yaml.tmpl), authenticating either from a kubeconfig / in-cluster service account or
from an API-server URL plus a bearer token held in Variables → Secrets.
| Variable | Purpose |
|---|---|
SECRETS_ENCRYPTION_KEY | Encrypts the secrets stored in the database. Set it before storing anything; losing it means losing the stored secrets |
OAUTH_CALLBACK_BASE_URL | Must match the origin the browser reaches the UI on |
FRONTEND_BASE_URL | Where to send the user back after an OAuth round trip |
ATLASSIAN_CLIENT_ID / ATLASSIAN_CLIENT_SECRET / ATLASSIAN_MCP_URL | Jira/Confluence over MCP |
config.yaml, agent: section)| Key | Default | Meaning |
|---|---|---|
maxIterations | 30 | Completion rounds in the main agent loop; tool calls re-trigger the LLM until it returns none |
planFanoutConcurrency | 4 | Stage sub-agents planning in parallel |
stageMaxIterations | 15 | Round budget for one stage sub-agent |
planFanoutTimeoutMinutes | 60 | Deadline for a whole fan-out, which outlives the request that started it |
actionExecConcurrency | 1 | Clamped to 1 by design — two agents changing the same infrastructure can undo each other's work, and an operator watching one chat cannot follow two |
actionExecMaxIterations | 20 | Round budget for one action execution |
actionExecTimeoutMinutes | 30 | Deadline for one action run |
/app/data is declared as a VOLUME, so even a bare docker run gets persistent storage. The
entrypoint builds the layout at runtime, because the volume starts empty on a fresh install:
/app/data
├── logs/ # log.file writes here; the backend does not create this dir itself
├── rules/ # infrastructure rules, editable in the UI
├── skills/ # {name}.md playbooks with name/description frontmatter
├── prompts/ # runtime override for the Discuss system prompt (discuss.md)
├── tools/ # per-mode allow lists + action-plan tool schemas
├── knowledgebase/ # last uploaded source document per collection
└── mcp.json # remote MCP server definitions
Seeding is copy-if-absent and recorded, so a skill you edited, renamed or deleted stays that way across restarts and upgrades, while a skill added in a later release still lands on an existing install. Per-mode tool allow lists are not copy-if-absent — they are embedded in the binary and reconciled at startup, because copy-if-absent left newly shipped tools dead on upgraded installs.
Mount a named volume or a PVC. Bind-mounting single files inside DATA_DIR will break: the backend
rewrites them with a temp file plus rename(2), which fails with EBUSY on a bind-mounted file.
| Port | Purpose |
|---|---|
8080 | REST API under /api/v1 (plans, chats, DAG, rules, skills, secrets, variables, MCP servers, executor config, OAuth callbacks, the agent-runner webhook) |
9090 | Prometheus metrics at /metrics |
Healthcheck: curl -sf http://localhost:8080/api/v1/health, 15 s interval with a 60 s start period.
Version is served at GET /api/v1/version and shown at the bottom of the UI sidebar.
Metrics live on a separate listener rather than a route on the API: both the chart's ingress and
the frontend's nginx proxy only the /api prefix, so /metrics stays unreachable from the public
host while an in-cluster scraper on the pod port still gets it. It is unauthenticated — the metric
names alone reveal plan counts, the chosen model and accumulated LLM cost — so keep the port
internal. The chart can render a ServiceMonitor (only when monitoring.coreos.com/v1 exists in
the cluster) or you can use prometheus.io/scrape pod annotations.
Exposed families include the Go and process collectors plus:
nib_build_info, nib_start_time_seconds, nib_db_up, nib_db_ping_duration_seconds
nib_http_requests_total, nib_http_request_duration_seconds, nib_http_requests_in_flight
nib_plans, nib_plan_status_transitions_total, nib_plans_refresh_*
nib_agent_turns_total, nib_agent_rounds, nib_agent_tool_calls_total, nib_agent_tool_failures_total
nib_llm_requests_total, nib_llm_request_duration_seconds, nib_llm_embedding_tokens_total
nib_plan_fanout_*, nib_action_exec_*, nib_executor_runs_total, nib_agent_runner_cost_usd_total
nib_execution_lease_held, nib_execution_lease_acquisitions_total, nib_execution_force_stops_total
nib_mcp_tools_discovered, nib_db_pool_*
Plans-by-status costs one query plus one plan-state read per plan, so it refreshes on an interval
(metrics.refreshSeconds, default 30 s) rather than on every scrape. METRICS_PORT must differ
from SERVER_PORT or the backend refuses to start. Full reference: docs/metrics.md.
nib — the API server and agent orchestratortoolcatalog — MCP tool indexing helper/app/migrations, /app/config.yaml, /app/seed (default skills, tool schemas, mcp.json)debian:bookworm-slim rather than Alpine, with bash, curl, jq, dnsutils,
iproute2, netcat-openbsd, openssl, procps and telnet — an infrastructure tool whose
container you cannot debug from the inside is a poor trade for a few MBCGO_ENABLED=0 Go binaries, cross-compiled natively for each target arch (no QEMU)internal/versionlatest · vX.Y.Z (release, matching the repository's VERSION file) · <git-sha> (exact commit).
Built for linux/amd64 and linux/arm64.
Pin vX.Y.Z in production and keep backend, frontend and kb on the same tag — they are released
together from one repository.
deploy/ and docker-compose.yml in the repositoryLICENSE before deploying this in a commercial setting.Content type
Image
Digest
sha256:707ef4408…
Size
77 MB
Last updated
15 days ago
docker pull javdet/nib-backend