Skip to content

Self-host Kindgi

You run four containers on one Docker network:

  • the runtime: Kindgi's server, with its API, agents and flows;
  • Postgres;
  • your pack's service: your tools' code;
  • a registry the runtime reads your pack's image from.

You then deploy a pack to it, and run a flow end to end. Everything here runs on one machine with Docker Desktop. On a server the pieces are the same; step 2 says what changes.

  • Docker, and Node 22 or later.

  • A pack. This page uses the sample:

    Terminal window
    npx @kindgi/cli init acme-pack --template=sample
    cd acme-pack
    pnpm install
  • A license key. Outside development mode, the runtime needs one: a free non-production key covers staging and CI, and a production key comes with a commercial license. To get one: contact@kindgi.com. See Licensing. kindgi dev needs none.

Terminal window
docker login quay.io
docker pull quay.io/kindgi/runtime:0.1.0
Terminal window
docker network create kindgi
export DB_PASSWORD="$(openssl rand -hex 24)"
docker run -d --name kindgi-db --network kindgi \
-e POSTGRES_USER=kindgi -e POSTGRES_PASSWORD="$DB_PASSWORD" -e POSTGRES_DB=kindgi \
-v kindgi-db:/var/lib/postgresql/data \
pgvector/pgvector:pg16
docker run -d --name kindgi-registry -p 127.0.0.1:5050:5000 registry:2

The runtime verifies your pack's image by reading it from a registry. On one machine with Docker Desktop, the name registry.localhost reaches the local registry from two places:

  • Docker: it treats *.localhost as loopback, so it pushes there over plain HTTP;
  • the runtime's container: --add-host (step 5) maps the name to your machine.

On Linux or a server, use your own registry instead, and give the runtime its credentials (KINDGI_IMAGE_REGISTRY_HOST, _USERNAME, _PASSWORD).

Pick a tenant id. The runtime serves this tenant, and the pack's signature names it:

Terminal window
uuidgen | tr 'A-Z' 'a-z'

Create a signing key:

Terminal window
pnpm exec kindgi key create acme-selfhost --env selfhost

Add an environment for this deployment to kindgi.config.ts:

environments: {
selfhost: {
endpoint: 'http://localhost:4000',
registry: 'registry.localhost:5050',
tenantId: '<your tenant id>',
signingKey: '~/.kindgi/keys/acme-selfhost.pem',
signerKeyId: 'acme-selfhost',
},
},

Build the image with your own Docker, push it, and sign it:

Terminal window
pnpm exec kindgi build --local --push --env selfhost
✓ Pushed registry.localhost:5050/acme-pack@sha256:11c12784…
✓ /app/index.json in the image matches the local index byte for byte
✓ Ed25519 signature over (imageDigest, artifactVersion, indexHash, tenantId, publishedAt)
Deploy envelope written to …/acme-pack/.kindgi/build/deploy-envelope.json

The image is for linux/amd64 by default. On Apple silicon it runs under emulation; --platform picks another.

Your tools' code runs in the pack's own container. The runtime calls it with a token both sides share:

Terminal window
echo "KINDGI_PACK_SERVICE_TOKEN=$(openssl rand -base64 32)" > pack.env
docker run -d --name kindgi-pack --network kindgi --env-file pack.env \
registry.localhost:5050/acme-pack@sha256:<the digest kindgi build printed>

Its log says it's listening:

{"kind":"listening","port":8080,"packId":"acme-pack","artifactVersion":"20261003.1"}

Make an API token. Your CLI and apps send it as their bearer:

Terminal window
printf 'kgi_bt_%s\n' "$(openssl rand -hex 32)"

Put the runtime's settings in kindgi.env:

Terminal window
KINDGI_DATABASE_URL=postgres://kindgi:<DB_PASSWORD>@kindgi-db:5432/kindgi
KINDGI_TENANT_ID=<your tenant id>
KINDGI_API_TOKEN=<the token>
KINDGI_ENV=production
KINDGI_PACK_SERVICE_URL=http://kindgi-pack:8080
KINDGI_PACK_SERVICE_TOKEN=<the same token as in pack.env>
KINDGI_IMAGE_REGISTRY_INSECURE_HOSTS=registry.localhost:5050
KINDGI_LICENSE_KEY=<your license key>

Every setting is in the environment variable reference. Two are worth knowing now:

  • KINDGI_ENV names the environment your tools' secrets resolve in.
  • KINDGI_TENANT_HOST_ACCESS isn't set here, so it's deployed, the default outside development. It refuses an MCP endpoint that would run a command on the runtime's host (stdio). Run MCP servers over HTTP instead. local allows it; set that only on a machine where everyone with an API token may run commands.

Start the runtime:

Terminal window
docker run -d --name kindgi-server --network kindgi \
--add-host registry.localhost:host-gateway \
-p 127.0.0.1:4000:4000 --env-file kindgi.env \
quay.io/kindgi/runtime:0.1.0
Terminal window
curl -s http://localhost:4000/health
{"ok":true}

Its log names what it's running with:

Terminal window
docker logs kindgi-server
Kindgi API server listening on http://localhost:4000
Tenant: 8f34192d-53bb-4fc2-bfb8-9094157b2404
Token: kgi_bt_…abb1 (provided)
…
Deployments: on (signed images, /v1/deployments)
License: Docs example · non-production · until 2026-11-02
Env: production (tool secrets resolve in it)
Tenant host access: deployed (stdio MCP endpoints refused; KINDGI_TENANT_HOST_ACCESS)
Pack service: http://kindgi-pack:8080 — acme-pack (artifact 20261003.1), protocol 2, 3 tools, 1 check

Without KINDGI_LICENSE_KEY, the runtime doesn't start. It exits with code 2 and says:

KINDGI_LICENSE_KEY is not set. Outside development mode the Kindgi runtime needs a license key: a production key comes with a commercial license, and a free non-production key covers staging and CI. To get one: contact@kindgi.com. Local development needs none: `kindgi dev` runs the runtime with KINDGI_DEV=true.

A key within 30 days of expiry adds a warning under the license line.

The runtime deploys only images signed by a key its tenant trusts. Trust yours with its 32 raw bytes, base64:

Terminal window
export KINDGI_API_TOKEN=<the token from kindgi.env>
PUB="$(pnpm exec kindgi key export acme-selfhost --format=raw-hex | xxd -r -p | base64)"
curl -s -X POST http://localhost:4000/v1/signing-keys \
-H "authorization: Bearer $KINDGI_API_TOKEN" -H 'content-type: application/json' \
-d "{\"keyId\":\"acme-selfhost\",\"publicKey\":\"$PUB\"}"

Then deploy:

Terminal window
pnpm exec kindgi deploy --env selfhost --token "$KINDGI_API_TOKEN"
"status": 201,
"outcome": "created",
…
"primitives": { "tools": 3, "guardrails": 1, "agents": 1, "flows": 1 },

The runtime checked the signature, read the pack's index from the image, and registered its tools, agents and flows.

The sample's agent needs a model that can call tools. Any OpenAI-compatible endpoint whose model supports tool calling works. This example uses Ollama on the same machine (after ollama pull llama3.1), which needs no key. The runtime reaches it at host.docker.internal: Docker Desktop provides that name, and on Linux, add --add-host host.docker.internal:host-gateway to the runtime's docker run. Save it as ollama.json:

{
"adapter_id": "@kindgi/adapter-model-openai-compat",
"adapter_config": { "baseURL": "http://host.docker.internal:11434/v1" },
"metadata": {
"id": "ollama-local",
"region": "unspecified",
"models": [{ "name": "llama3.1", "contextWindow": 131072, "features": ["tool-use"],
"cost": { "promptUsdPer1kTokens": 0, "completionUsdPer1kTokens": 0 } }]
}
}
Terminal window
pnpm exec kindgi providers register --spec=@ollama.json --url http://localhost:4000 --token "$KINDGI_API_TOKEN"
pnpm exec kindgi runs start --flow=acme-pack.echo-flow --input='{"name":"Ada"}' --url http://localhost:4000 --token "$KINDGI_API_TOKEN"
"status": "completed",
…
"output": {
"reply": "…Hello, Ada!…",
"greeting": "Hello, Ada!"
}

The flow's tool step ran in your pack's container, and its agent step answered with the model. A first call can outlast the agent's time budget while the model loads; run it again.

Terminal window
docker rm -f kindgi-server kindgi-pack kindgi-registry kindgi-db
docker volume rm kindgi-db
docker network rm kindgi

Coming soon.