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.
Prerequisites
Section titled “Prerequisites”-
docker(orpodman) withdocker compose(orpodman compose) available — version 2.20+ recommended. On macOS use Docker Desktop, orpodman 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.
1. Sign up at app.kici.dev
Section titled “1. Sign up at app.kici.dev”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.
3. Download the compose template
Section titled “3. Download the compose template”mkdir my-kici && cd my-kicicurl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/docker-compose.yamlcurl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/scalers.yamlcurl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/seaweedfs-s3.jsoncurl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/.env.examplecp .env.example .envdocker-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— thekici_ok_…token from step 2.KICI_SECRET_KEY—openssl rand -hex 32(must be 64 hex chars; encrypts secrets at rest).KICI_BOOTSTRAP_ADMIN_TOKEN—openssl rand -hex 32(first-time admin token for thekici-adminCLI).
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.
4. Boot the stack
Section titled “4. Boot the stack”docker compose up -ddocker compose logs -f orchestratorYou 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.
5. Run a workflow without pushing
Section titled “5. Run a workflow without pushing”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.
# 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-kicigit init -q -b mainprintf '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 loginkici 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-mainYou 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:
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.7. Wire GitHub to the webhook URL
Section titled “7. Wire GitHub to the webhook URL”In your GitHub App’s settings:
- Webhook URL: the URL printed above.
- Webhook secret: the same
--webhook-secretyou passed in step 6. - Subscribe to events: at minimum
pushandpull_request.
Click Save changes in GitHub.
8. Push a commit
Section titled “8. Push a commit”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:
mkdir -p .kici/workflowscat > .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 compilegit add .kici/ && git commit -m "ci: hello-world workflow" && git pushWatch 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.
What just happened
Section titled “What just happened”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 PlatformThe 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.
Upgrading
Section titled “Upgrading”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:
curl -O https://raw.githubusercontent.com/kici-dev/kici-public/main/examples/quickstart/compose/docker-compose.yamldocker compose pulldocker compose up -ddocker 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.
Where to next
Section titled “Where to next”- Write more workflows — see the SDK reference for triggers, jobs, conditionals, matrices, and dynamic values.
- Test before pushing —
kici test pr:openpreviews which workflows would fire for a given event;kici run localexecutes a workflow against your laptop without any infrastructure. See Getting started. To run your local working tree through this orchestrator without a git push, usekici 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.
Troubleshooting
Section titled “Troubleshooting”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(orpermission denied): the orchestrator can’t reach/var/run/docker.sock. On rootless Podman the socket lives at$XDG_RUNTIME_DIR/podman/podman.sockinstead — editdocker-compose.yamlto swap the bind-mount (e.g.- /run/user/1000/podman/podman.sock:/var/run/docker.sock) anddocker compose up -dagain. Enable the podman user socket first withsystemctl --user enable --now podman.socketif it isn’t already running.scaler config parse error:scalers.yamldidn’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) anddocker compose restart orchestrator.- The spawned agent comes up but immediately disconnects with
connection refused: spawned containers can’t reachhost.docker.internal:4000. On Docker Desktop (Mac/Windows) and Podman 4+ this should work out of the box; on plain Linux Docker engine theextraHosts: ['host.docker.internal:host-gateway']line inscalers.yamladds the alias. If that’s still failing, your firewall is dropping bridge-to-host traffic —sudo iptables -L DOCKER-USERand add a permissive rule, or temporarily setnetwork_mode: hoston 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.