Skip to content

Quickstart: Docker / Podman

This guide gets you to a working end-to-end pipeline in about five minutes using docker compose (or podman compose). If you’d rather run native systemd services, see the bare-metal quickstart. For the chooser overview, see 5-minute quickstart.

You’ll end up with:

  • A KiCI orchestrator running on your machine (or a tiny VM), configured to spawn agent containers on demand via the container scaler.
  • Connected to the public Platform at api.kici.dev, which receives GitHub webhooks and relays them to your orchestrator over an outbound WebSocket. No inbound port needs to be exposed on your side — the orchestrator opens the connection.
  • A GitHub push that triggers your first workflow run, with logs visible in the dashboard at https://app.kici.dev.
  • docker (or podman) with docker compose (or podman compose) available — version 2.20+ recommended. On macOS use Docker Desktop, or podman machine (podman machine init && podman machine start).

  • A GitHub repository you can install a GitHub App on.

  • A GitHub App you’ve created for that repository. You need two things from it now:

    • its App ID (shown on the App’s settings page), and
    • a private key (.pem) — click Generate a private key and save the download.

    Leave the App’s Webhook URL field blank for now — you’ll generate that URL in step 6 (“Register your GitHub App as a webhook source”) and paste it back into GitHub then.

  • 5 minutes.

Go to app.kici.dev and create an account.

After sign-up you’ll have a personal organisation. Future-you can invite teammates and create additional orgs from the dashboard.

2. Mint an orchestrator registration token

Section titled “2. Mint an orchestrator registration token”

In the dashboard, open Settings → Orchestrators → New orchestrator, give it a name (e.g. home-server), and copy the token the dialog shows. The token starts with kici_ok_ and is shown only once — save it now.

This token authorises your orchestrator to connect to wss://api.kici.dev/ws and identify itself as belonging to your organisation.

Terminal window
mkdir my-kici && cd my-kici
curl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/docker-compose.yaml
curl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/scalers.yaml
curl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/seaweedfs-s3.json
curl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/.env.example
cp .env.example .env

docker-compose.yaml brings up the orchestrator, a local Postgres, and a SeaweedFS object store (so kici run remote works locally — see step 5). scalers.yaml declares the container scaler — when a job arrives, the orchestrator spawns a one-shot agent container on this host, the job runs inside it, and the container is destroyed when the job finishes. seaweedfs-s3.json is the SeaweedFS credential config the compose mounts.

Open .env in your editor and fill in the three values:

  • KICI_PLATFORM_TOKEN — the kici_ok_… token from step 2.
  • KICI_SECRET_KEYopenssl rand -hex 32 (must be 64 hex chars; encrypts secrets at rest).
  • KICI_BOOTSTRAP_ADMIN_TOKENopenssl rand -hex 32 (first-time admin token for the kici-admin CLI).

POSTGRES_PASSWORD is optional — it defaults to the local-only stub kici-local (Postgres isn’t published outside the compose network). Set it in .env only if you want a custom password.

Terminal window
docker compose up -d
docker compose logs -f orchestrator

You should see something like this within ~5 seconds:

[orchestrator] connected to platform api.kici.dev (registration <id>)
[orchestrator] listening on :4000
[orchestrator] scaler container-default loaded (type=container, maxAgents=4, labels=linux,container)

Ctrl-C to stop tailing logs; the stack keeps running in the background.

The dashboard’s Orchestrators page now shows your registration as online. No agent container is running yet — the scaler will spawn one when your first job arrives in step 5.

The stack includes a SeaweedFS object store, so you can run a workflow against your local orchestrator straight from your working tree — no GitHub App, no git push, no webhook. This is the fastest way to confirm the whole pipeline works.

Terminal window
# A throwaway git repo to hold the workflow (any folder with a .kici/ works).
# `kici run remote` reads your working tree on top of a commit, so the folder
# must be a git repo with at least one commit.
mkdir -p hello-kici/.kici/workflows hello-kici/.kici/tests && cd hello-kici
git init -q -b main
printf 'node_modules/\n' > .gitignore
# The SDK lives in .kici/ — both the compiler (on your machine) and the agent
# (inside the spawned container) resolve `@kici-dev/sdk` from .kici/node_modules.
cat > .kici/package.json <<'EOF'
{
"name": "hello-kici-workflows",
"private": true,
"type": "module",
"devDependencies": {
"@kici-dev/sdk": "^0.1.14"
}
}
EOF
cat > .kici/workflows/hello.ts <<'EOF'
import { workflow, job, step, push } from '@kici-dev/sdk';
export default workflow('hello', {
on: push({ branches: ['main'] }),
jobs: [
job('greet', {
runsOn: 'linux',
steps: [
step('say hi', async ({ $ }) => {
await $`echo "Hello from KiCI 👋"`;
}),
],
}),
],
});
EOF
cat > .kici/tests/push.ts <<'EOF'
import { fixture, push } from '@kici-dev/sdk';
export const pushMain = fixture('push-main', { event: push({ branches: ['main'] }) });
EOF
# Install the SDK and make the first commit (run remote needs a HEAD commit).
( cd .kici && npm install )
git add -A && git commit -q -m "hello kici"
# Log in to the Platform and select your organization.
kici login
kici org use <your-org>
npx kici compile
# Run it — the run routes through the Platform to your orchestrator, the CLI
# uploads your working tree directly to SeaweedFS, the scaler spawns a one-shot
# agent container, and logs stream back to your terminal.
kici run remote push-main

You should see a green push-main … success run in your terminal.

kici run remote uses two planes. The control plane (run initiation, status, logs, cancellation) flows from your machine through the Platform, which relays it over a WebSocket connection to your local orchestrator. The data plane — your working-tree overlay — uploads directly from your machine to SeaweedFS via a presigned URL and never passes through the Platform. That direct upload is exactly what KICI_STORAGE_UPLOAD_ENDPOINT=http://localhost:8333 enables: the host CLI uploads to localhost:8333, the orchestrator hands the agent a container-routable URL (host.docker.internal:8333), and the agent fetches the overlay before running your steps.

With a single connected orchestrator the Platform selects it automatically. If your org later connects more than one, list them with kici orchestrators list and pin a default with kici orchestrators use <name> (or pass --orchestrator <name> per run).

See the testing guide for fixtures, secret contexts, and more.

You’ve now run a workflow end-to-end on your own box. The rest of this guide connects real GitHub pushes so your team’s commits trigger runs automatically.

6. Register your GitHub App as a webhook source

Section titled “6. Register your GitHub App as a webhook source”

<YOUR_APP_ID> and the .pem are the two values from the prerequisites; the Webhook URL the command prints below is what you paste into the App’s blank Webhook URL field in the next step.

From the same machine you started the stack on:

Terminal window
npm install -g kici-admin
kici-admin --url http://localhost:4000 --token "$KICI_BOOTSTRAP_ADMIN_TOKEN" \
source add github \
--name my-org \
--app-id <YOUR_APP_ID> \
--private-key @./github-app-private-key.pem \
--webhook-secret <YOUR_WEBHOOK_SECRET>

Replace <YOUR_APP_ID> with the App ID from your GitHub App’s settings page, point --private-key at the .pem you downloaded from GitHub, and use any random string for --webhook-secret (you’ll paste the same string into the GitHub App’s webhook config in the next step).

The command prints the public webhook URL the Platform now accepts for this source:

Source added: github:<appId> (my-org)
Webhook URL: https://api.kici.dev/webhook/<orgId>/github
↳ Paste this into your GitHub App's "Webhook URL" field.

In your GitHub App’s settings:

  • Webhook URL: the URL printed above.
  • Webhook secret: the same --webhook-secret you passed in step 6.
  • Subscribe to events: at minimum push and pull_request.

Click Save changes in GitHub.

In a repo that has the GitHub App installed, drop in the same .kici/ folder you built in step 5 (the .kici/package.json + .kici/workflows/hello.ts + .kici/tests/push.ts), then push it:

Terminal window
mkdir -p .kici/workflows
cat > .kici/workflows/hello.ts <<'EOF'
import { workflow, job, step, push } from '@kici-dev/sdk';
export default workflow('hello', {
on: push({ branches: ['main'] }),
jobs: [
job('greet', {
runsOn: 'linux',
steps: [
step('say hi', async ({ $ }) => {
await $`echo "Hello from KiCI 👋"`;
}),
],
}),
],
});
EOF
# If this repo doesn't already have a .kici/package.json with @kici-dev/sdk,
# copy the one from step 5 (or run `npx kici init`) so the agent can resolve it.
npx kici compile
git add .kici/ && git commit -m "ci: hello-world workflow" && git push

Watch the run light up in the Runs page of the dashboard. This time the trigger came from GitHub: the push hit the Platform, which relayed it to your orchestrator, which spawned a one-shot agent container to clone the repo and run the step.

GitHub Platform (api.kici.dev) your box
│ │ │
│ POST /webhook/... │ │
├─────────────────────────►│ │
│ │ WebSocket relay (outbound) │
│ │◄────────────────────────────────│ orchestrator
│ │ │ │ scaler.spawn()
│ │ │ ▼
│ │ │ agent container
│ │ │ │ git clone + run steps
│ │ log chunks + status │ │
│ │◄────────────────────────────────│ │
│ │ │ ▼ (destroyed on exit)
dashboard reads run state from Platform

The Platform handles webhook ingress + signature verification + audit logging. Your orchestrator owns the trigger matching, job queue, and per-source secrets — and spawns one agent container per job via the bind-mounted container runtime socket. Each agent runs exactly one job, then exits.

When a new KiCI version ships, the published quay.io/kici-dev/kici-orchestrator and quay.io/kici-dev/kici-agent images move to the new tag and the compose template pins it. Re-download the template, pull, and recreate the stack:

Terminal window
curl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/docker-compose.yaml
docker compose pull
docker compose up -d

docker compose up -d recreates the orchestrator from the newer pinned image; your .env, the Postgres volume, and scalers.yaml are left untouched. Agent containers respawn from the new agent image on the next job, and DB migrations run automatically on first start of the new version.

  • Write more workflows — see the SDK reference for triggers, jobs, conditionals, matrices, and dynamic values.
  • Test before pushingkici test pr:open previews which workflows would fire for a given event; kici run local executes a workflow against your laptop without any infrastructure. See Getting started. To run your local working tree through this orchestrator without a git push, use kici run remote (step 5 above) — it works out of the box here thanks to the bundled SeaweedFS store. See the testing guide.
  • Switch to bare metal — if you’d rather run native systemd services without a container runtime, see the bare-metal quickstart.
  • Tune the scaler — add label sets for additional runtimes, enable warm pools to pre-spawn agents, set per-job CPU / memory limits, gate specialised hardware behind mandatory labels, or point the scaler at a remote container daemon. See Auto-scaler overview, Common configuration, and the Container backend.
  • Run isolated — Firecracker microVM execution for untrusted code or per-job isolation. See Firecracker setup.

Orchestrator logs show auth.failed immediately after start. The KICI_PLATFORM_TOKEN in .env doesn’t match what you minted at app.kici.dev. Mint a fresh one (the old one stays revokable from the dashboard) and update .env, then docker compose restart orchestrator.

Push happens but no agent container spawns. Tail docker compose logs -f orchestrator immediately after the push and look near the top for one of three failure modes:

  • scaler spawn failed: Cannot connect to the Docker daemon (or permission denied): the orchestrator can’t reach /var/run/docker.sock. On rootless Podman the socket lives at $XDG_RUNTIME_DIR/podman/podman.sock instead — edit docker-compose.yaml to swap the bind-mount (e.g. - /run/user/1000/podman/podman.sock:/var/run/docker.sock) and docker compose up -d again. Enable the podman user socket first with systemctl --user enable --now podman.socket if it isn’t already running.
  • scaler config parse error: scalers.yaml didn’t validate. Re-download from the kici-public repo (curl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/scalers.yaml) and docker compose restart orchestrator.
  • The spawned agent comes up but immediately disconnects with connection refused: spawned containers can’t reach host.docker.internal:4000. On Docker Desktop (Mac/Windows) and Podman 4+ this should work out of the box; on plain Linux Docker engine the extraHosts: ['host.docker.internal:host-gateway'] line in scalers.yaml adds the alias. If that’s still failing, your firewall is dropping bridge-to-host traffic — sudo iptables -L DOCKER-USER and add a permissive rule, or temporarily set network_mode: host on the orchestrator service.

Push happens but no run appears in the dashboard. Either GitHub didn’t deliver the webhook (check the App’s “Recent deliveries” tab in GitHub’s settings — look for 4xx responses), or the orchestrator received it but no workflow matched. Run kici test push against your workflow file to confirm a push to your branch would trigger something.

Postgres won’t start. If you set a custom POSTGRES_PASSWORD after a previous boot, the data volume from the earlier attempt carries the old password and is incompatible. docker compose down -v wipes the volume and lets you start fresh.