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_OPTIONSand 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 andrun.envof 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 exampleRUNNER_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.secretsandrun.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_TOKENand 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 andTMPDIRunder 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_KEYfor Claude Code,OPENAI_API_KEYfor 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:
- Open Your settings > API tokens (avatar menu, or Use an existing token in the Add a runner panel) and click Create token.
- Name it after the runner, for example
runner-fra-1, and pick the workspace the runner works for under Workspace. - 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.
- 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.
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_*, theKANMAN_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_*(exceptKANMAN_ASSISTANT_*),RUNNER_*(exceptRUNNER_COMMAND_ENV),PATH,HOME, proxy and certificate settings,GIT_*,SSH_*,DOCKER_*,LD_*,NODE_OPTIONSand 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_CONCURRENCYon 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