Run the self-hosted runner

Run kanman's code work on your own infrastructure with a Docker image that connects outbound only, without opening ports or sharing database credentials.

By default, kanman runs each story in an isolated machine in Frankfurt, Germany. If your code must not leave your network, run the coding work on your own infrastructure with the self-hosted runner.

How it works

The runner is the same executor host kanman uses in its cloud, packaged as the Docker image ghcr.io/kanman-ai/executor-host:

  • It pulls work. It opens an outbound HTTPS connection to kanman, registers, asks for the next queued run of your workspace and reports back. No inbound ports, no VPN, no firewall exceptions for kanman.
  • It authenticates with a workspace API token (km_...). It never receives database credentials. Each run it picks up gets its own run token, which stops working when the run ends.
  • For each run it clones the repository, creates the run branch, starts the coding agent with the run’s brief and the kanman MCP, and reports progress, the diff summary, the commits and a transcript of the agent’s work.
  • The repository checkout stays on your machine.

What the model provider receives is described in What model providers see. The coding agent calls the provider from the runner, with the key you give the runner or, for teams that use their own key, with that key.

How the runner waits for work

A run is a chain of steps: the coding work, then the acceptance checks, and in some workspaces drafts and reviews around it. The runner picks up each step within about a second:

  • Long poll (default). The runner asks for work and kanman keeps the request open for up to 25 seconds. As soon as a step is queued, kanman answers with it. When nothing arrives, the runner asks again right away.
  • Next step with the result. When the runner reports a finished step and the next one is already waiting, kanman hands it over in the same answer, so the next step starts without another request.
  • Fallback. If a proxy or firewall in your network cuts requests that stay open this long, the runner notices after three cut requests in a row, writes one line to its log (“Long poll unavailable, polling classic until it is tried again”) and switches to short requests every few seconds (at most every 10 seconds). It tries the long poll again after 10 minutes.

The run’s Cost & details tab shows where it ran and how quickly your runner picked up its steps, for example “4 steps, picked up after 0.4 s (median)”. The run’s progress keeps one line per attempt for when the run itself was picked up.

Proxy notes. Allow requests to kanman to stay open for at least 30 seconds (the idle or read timeout of your proxy). If that is not possible, set RUNNER_LONG_POLL_MS below your proxy’s limit, for example RUNNER_LONG_POLL_MS=10000, or turn the long poll off with RUNNER_LONG_POLL=off. Without the long poll, steps start up to 10 to 30 seconds later.

Acceptance checks on your runner

In a workspace that sends its runs to self-hosted runners, the acceptance checks of the outcome gate run there too: the spec run before the work (it must fail), the run in a fresh environment and the run on a clean checkout. Your application, its test data and the screenshots and traces are created on your infrastructure; kanman receives the result and the proof artifacts for the evidence pack.

  • A runner takes acceptance checks when it can reach Docker with Compose and the Docker daemon answers, because the checks start your application from the acceptance manifest. It checks this when it starts and again every five minutes, so fixing Docker access takes effect without a restart. While Docker is not usable, the runner log says why.
  • Each check starts from a fresh checkout in a temporary folder, never from the folder the coding agent worked in, and the environment is torn down afterwards.
  • If no runner that can take acceptance checks is online, kanman retries the check twice and then asks you in Decisions, like for any other problem of the test environment. A check whose runner stops in the middle is handed to another runner.

Teams on Trial without an acceptance manifest need no Docker on the runner: CI and the reviewer do not run on it.

Runner secrets

The runner’s environment holds credentials: its kanman token, your code host tokens and your model access. Commands that come from a repository never receive them. That covers the commands of the acceptance manifest (provision, setup, run and teardown), the local verify command and the application these commands build and start from a run branch.

  • What repository commands see. A minimal environment: PATH, HOME, user, shell, language and locale settings, time zone, temporary folder, proxy settings (HTTPS_PROXY, HTTP_PROXY, NO_PROXY, ALL_PROXY), certificate locations (NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, SSL_CERT_DIR, GIT_SSL_CAINFO, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE), LD_LIBRARY_PATH, PLAYWRIGHT_BROWSERS_PATH, NODE_OPTIONS and the Docker client settings Compose needs (DOCKER_HOST, DOCKER_CONTEXT, DOCKER_CONFIG, DOCKER_CERT_PATH, DOCKER_TLS_VERIFY, DOCKER_BUILDKIT, DOCKER_DEFAULT_PLATFORM, XDG_RUNTIME_DIR). Plus what the check itself provides: CI=true, the story key and run.env of the manifest.
  • More variables from the runner. If your build needs further settings of the runner host, list their names in RUNNER_COMMAND_ENV, for example RUNNER_COMMAND_ENV=JAVA_HOME,MAVEN_OPTS. kanman’s own variables and the runner’s code host tokens are never passed on. Do not list credentials there: everything you list is visible to repository code. A team can add names of its own further variables in its settings (Several teams on one runner).
  • Values your checks need. Test accounts, tokens of test systems and similar values belong in a workspace secret granted to the team, or in a CI variable of the repository. Name them in the manifest with run.secrets and run.ci_var_keys; see Secrets and CI variables. The setup and spec commands receive them, the values are removed from logs and output, and they are not stored with the evidence.
  • Git access. The runner uses its code host token only for its own clone, fetch and push steps. The token is not written into the checkout, so neither the coding agent nor repository code can read it there.
  • The coding agent. Claude Code or Codex never sees the runner’s code host tokens (GITHUB_TOKEN, GH_TOKEN, GITLAB_TOKEN, RUNNER_GIT_TOKEN and the usual CI and CLI names for them), also not when it uses the runner’s model access. The agent commits in its checkout; the runner pushes the run branch when the agent is done.
  • Commits of the coding agent’s work. When the runner commits what the coding agent left behind, git hooks of the checkout do not run.

Requirements

  • A team on the Starter plan or higher, or in its 14-day Team trial. See Plans and billing.
  • A Linux host or VM with Docker. The runner needs CPU and memory like a CI job for your repository.
  • For acceptance checks: Docker on the host. The runner image brings the Docker client and the Compose plugin; mount the host’s Docker socket and give the runner the socket’s group: -v /var/run/docker.sock:/var/run/docker.sock --group-add "$(stat -c %g /var/run/docker.sock)".
  • For teams with a team environment: Docker, and nothing else. The runner starts the coding agent and every check in a container of the team’s image, so no toolchain is installed on the runner. When the runner itself runs in a container, mount its workspace folder at the same path inside and outside (for example -v /srv/kanman:/srv/kanman -e WORKSPACE_PATH=/srv/kanman), because the team’s containers mount the checkout by its path. Docker installed as a snap package cannot see /tmp: keep the workspace and TMPDIR under the runner’s home folder.
  • Outbound HTTPS to kanman, to your git host, and to your model provider. A runner inside a company network can reach a GitLab that kanman’s cloud cannot see, through a proxy and with internal certificates; see GitLab behind a VPN or firewall.
  • A model provider key for the executors the runner offers: ANTHROPIC_API_KEY for Claude Code, OPENAI_API_KEY for Codex. Not needed for teams that use their own key in kanman; their key comes with the run. Teams set to Model access from the runner environment use what you configure on the runner, for example your Amazon Bedrock, Google Vertex AI or Microsoft Foundry account, for coding runs and, with Everything on ours, for every other model step too; see Model access on self-hosted runners.

Step 1: Create a token

The quickest way: open Workspace settings > Runs and runners (or ask kanman “Show me the runners”) and click Add a runner. A panel leads you through three steps: Create a token, Start the runner and Connect. In the first step, give the runner a name under Name of the runner (it starts as “Runner” and the day), then click Create a token for this runner. kanman creates a token for this workspace that is valid for one year and carries the runner’s name, and the command starts the runner under the same name (RUNNER_NAME), so you can tell several runners and their tokens apart. kanman puts the token straight into the command in the next step. Copy the command right away: the token is shown only once. It is listed under Your settings > API tokens, folded below your personal tokens under runner and Claude Code tokens, where you can revoke it.

To name the token yourself or choose a shorter expiry:

  1. Open Your settings > API tokens (avatar menu, or Use an existing token in the Add a runner panel) and click Create token.
  2. Name it after the runner, for example runner-fra-1, and pick the workspace the runner works for under Workspace.
  3. Choose an expiry (30 days, 90 days or 1 year) that matches your rotation policy. The runner needs no particular permission; any active token of the workspace works.
  4. Copy the token. It is shown once.

Step 2: Start the runner

The second step of the Add a runner panel shows the command with your token, ready to copy. The third step says Waiting for it to connect… and checks every few seconds; as soon as the runner you start comes online, the panel shows its name and the runner appears in the list. With a name and both executors the command looks like this:

docker run -d --name kanman-runner \
  --restart unless-stopped \
  -e KANMAN_API_KEY=km_your_runner_token \
  -e ANTHROPIC_API_KEY=your_anthropic_key \
  -e RUNNER_NAME=runner-fra-1 \
  -e RUNNER_EXECUTORS=claude-code,codex \
  -e OPENAI_API_KEY=your_openai_key \
  ghcr.io/kanman-ai/executor-host
Variable Required Meaning
KANMAN_API_KEY Yes The workspace API token from step 1
ANTHROPIC_API_KEY, OPENAI_API_KEY For pass-through teams Provider key for Claude Code or Codex
RUNNER_NAME No Name shown in Workspace settings > Runs and runners. Default: the host name of the container
RUNNER_EXECUTORS No Comma-separated executors this runner offers: claude-code, codex. Default claude-code
RUNNER_CONCURRENCY No How many runs this runner works on at the same time. Default 1
LOG_LEVEL No debug, info, warn or error. Default info
GITHUB_TOKEN, GITLAB_TOKEN For Everything on ours The runner’s own code host tokens; GITHUB_API_URL and GITLAB_API_URL for GitHub Enterprise and self-managed GitLab
RUNNER_JOBS, RUNNER_JOB_CONCURRENCY No Which short steps (drafts, reviews, repository actions, acceptance checks) the runner takes, and how many next to its runs. Default: all it can do, 2
RUNNER_COMMAND_ENV No Comma-separated names of further runner variables that repository commands may see, for example JAVA_HOME,MAVEN_OPTS. See Runner secrets
RUNNER_LONG_POLL, RUNNER_LONG_POLL_MS No How the runner waits for work: RUNNER_LONG_POLL=off turns the long poll off; RUNNER_LONG_POLL_MS shortens how long a request stays open (default and maximum 25000). See How the runner waits for work
RUNNER_IDLE_MIN_MS, RUNNER_IDLE_MAX_MS No Pause between short requests when the long poll is off. Default 5000 and 30000; after a fallback at most 10000 (RUNNER_FALLBACK_IDLE_MAX_MS)

Pin the image to a version tag in production and update deliberately.

With team environments, acceptance checks or images from your own registry, mount the Docker socket and the workspace folder as well:

docker run -d --name kanman-runner \
  --restart unless-stopped \
  -e KANMAN_API_KEY=km_your_runner_token \
  -e RUNNER_NAME=runner-fra-1 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --group-add "$(stat -c %g /var/run/docker.sock)" \
  -v /srv/kanman:/srv/kanman -e WORKSPACE_PATH=/srv/kanman \
  ghcr.io/kanman-ai/executor-host

Step 3: Check the status

Open Workspace settings > Runs and runners. The runner appears with its name, executors, version, the time it was last seen and its status. The list refreshes every 30 seconds:

Status Meaning
Online Connected and accepting runs. The runner reports every 30 seconds.
Finishing work The runner was asked to stop. It finishes the runs it has and takes no new ones.
Offline Not connected, or no report for two minutes. Runs wait in the queue until a runner is online.

Online runners come first, then those finishing work, then offline runners by when they were last seen. Only the three most recent offline runners show; the older ones open with Show older offline runners. When work has to run on your runners and none is online, the page says so: the work waits until a runner connects. To remove a runner that is offline for good, open the ⋯ menu on its row and choose Remove. It disappears from the list, and the jobs it ran keep their history; if it starts again with its token, it registers anew.

The diagram Where things run at the top of the page shows kanman Cloud and your network side by side. kanman Cloud always keeps your teams and stories, decisions, the audit log and the run status and results. Your network shows your runners with their state. The kinds of work (runs, checks, model calls, repository actions and chat replies) sit where they execute and move across when you change where runs execute. What stays where below it explains the split in words.

Workspace settings, Runs and runners with one online runner showing its name, version and executors

Step 4: Send work to your runners

The choice applies to the whole workspace. Open Workspace settings > Runs and runners and, under Where runs execute, choose one of three:

Choice What runs where
kanman Cloud Every story runs on its own machine in kanman Cloud. The default.
Our runners Runs and their checks execute on your runners. kanman keeps planning and talking in kanman Cloud.
Everything on ours Everything executes on your runners, including model calls and repository actions. See Run everything on your runners.

Moving work onto your runners asks first and names what moves, because work waits while no runner is online. From then on, queued runs of all teams go to your runners and are picked up in order. Concurrency still follows each team’s policy and its plan. On top of that, Runs at the same time in total in the Advanced fold limits the whole workspace; it is No limit by default, since each team’s plan already caps its runs.

Several teams on one runner

One runner can serve every team of your workspace, while each team brings what it needs: its own model access (for example a person’s Claude subscription login to try kanman as their own tool), its own code host token, further variables and the credentials of the registry its team environment image comes from. Owners and admins set them per team in Team settings:

What Where in Team settings
Model access Agent and models > Model access, with Model access from the runner environment: This team’s model access on your runners. See Model access per team.
Code host tokens (GITLAB_TOKEN, GITHUB_TOKEN, GITLAB_API_URL, GITHUB_API_URL) Agent and models > On your runners > Code host tokens
Further variables Agent and models > On your runners > More variables
Registry credentials Checks and delivery > Team environment > Registry credentials

How it works:

  • The team’s value wins, the runner fills the rest. A value set for the team replaces the runner’s variable of the same name for that team’s runs and steps; everything the team does not set comes from the runner’s own environment. A team without settings works exactly as before.
  • Model access is replaced as a whole. When a team sets any model variable, none of the runner’s model variables (provider keys, AWS_*, the KANMAN_ASSISTANT_* model) are used for that team, so two accounts never mix.
  • Values stay encrypted in kanman and are handed to the runner with each run or step of that team only. The runner keeps them in memory for that work and never writes them to disk. Nobody sees a value again: the settings show the name, who set it and when.
  • Names that steer the runner itself cannot be set for a team: KANMAN_* (except KANMAN_ASSISTANT_*), RUNNER_* (except RUNNER_COMMAND_ENV), PATH, HOME, proxy and certificate settings, GIT_*, SSH_*, DOCKER_*, LD_*, NODE_OPTIONS and similar.
  • Every change is recorded in the audit log with the name, the team and the person, never the value.

What each kind of value reaches:

Value Reaches
Model variables The coding agent’s model access, through the runner’s credential proxy where the provider allows it (the agent sees a placeholder, not the value), and kanman’s own steps on the runner (drafts, review, specs, replies)
Code host tokens Only the runner’s own clone, fetch, push and code host calls for that team. The coding agent and repository commands never see them.
More variables The coding agent, when the team uses Model access from the runner environment and has no team environment (in a team environment, use its Variables and Secrets passed in). Repository commands only when the name is listed in RUNNER_COMMAND_ENV, the runner’s or the team’s own (a team can list its own further variables there, never a model or code host variable).
Registry credentials Only the pull of the team’s image, through a Docker configuration of its own that is removed right after the pull. They never become a variable and are never written to the runner’s Docker configuration.

Run everything on your runners

With Our runners, some steps around a run still happen in kanman’s cloud, for example drafting stories and the independent review. To keep all model traffic and every repository action in your network, choose Everything on ours under Where runs execute in Workspace settings > Runs and runners. See Run everything on your runners.

See kanman in your own monitoring

Every run has one trace that ties together what happened in kanman Cloud, on your runner and in the coding agent. If your company runs an OpenTelemetry collector (for example with Grafana, Elastic or Datadog), your runner can send each run’s trace there, so your operations team sees kanman’s work next to your own services without another dashboard.

Set these variables on the runner and restart it:

Variable Value
KANMAN_OTLP_ENDPOINT Address of your collector’s OTLP/HTTP receiver, for example https://otel.example.com:4318
KANMAN_OTLP_HEADERS Optional headers, name=value pairs separated by commas, for example x-api-key=…

After each run, the runner sends one span for the run, one for the model usage (tokens, cost and the region the model processed in) and one per check and command it ran. Commands are named by their program only, never with their arguments, and no prompts, answers or file contents are sent. The coding agent never sees these variables. Without KANMAN_OTLP_ENDPOINT, nothing is sent.

Scale and update

  • More capacity: start more runners with different names, or raise RUNNER_CONCURRENCY on a larger host.
  • Update: stop the container with enough time to finish its runs, for example docker stop --time 1800 kanman-runner. On stop the runner shows Finishing work, completes its runs and goes offline. Then start the new version.
  • Rotate the token: create a new token, restart the runner with it, then revoke the old one. A revoked or expired token takes the runner offline.

Troubleshooting

Symptom Likely cause
The runner does not start KANMAN_API_KEY is missing or does not start with km_. Check the container logs with docker logs kanman-runner.
The runner stays offline The token is wrong, expired or revoked, or outbound HTTPS to kanman is blocked. Check the container logs.
Runs stay queued No runner is online, or no online runner offers the team’s executor. Check RUNNER_EXECUTORS.
Steps start 10 seconds or more after they are queued A proxy cuts the long poll and the runner switched to short requests (its log says “Long poll unavailable”). Raise the proxy’s timeout for kanman to at least 30 seconds, or set RUNNER_LONG_POLL_MS below it. See Proxy notes.
Runs fail right after they start The runner has no provider key for the executor, or the runner host cannot reach your git host or model provider. When it starts, the runner log lists the model credentials it found by variable name (for example “Claude: credentials present in the environment”), never their values.
Team environments or acceptance checks fail with “permission denied” on the Docker socket The runner user is not in the socket’s group. Start the container with --group-add "$(stat -c %g /var/run/docker.sock)".
Acceptance checks never run on this runner The runner cannot use Docker, for example because the Docker socket is not mounted or the daemon is stopped. The runner log names the reason; once Docker works, the runner takes checks again within five minutes.

Last updated: January 1, 0001

Open kanman