Skip to content

Latest commit

 

History

History
444 lines (357 loc) · 18.9 KB

File metadata and controls

444 lines (357 loc) · 18.9 KB

Running Kiro Crew in Docker

The official image runs the Kiro Crew gateway — dashboard, channel bots (Slack / Discord / Telegram / WeCom / Weixin / Webex), crons, and the kiro-cli agent runtime — as a headless container. Microsoft Teams, Feishu, and WhatsApp need optional dependencies that the stock image does not install; iMessage requires macOS and cannot run in this Linux image. It is the recommended way to run Kiro Crew 24/7 on a server or NAS; the strongest fit is the always-on channel bot that does not need a desktop session.

The image is public, so no registry login is needed. Start the gateway:

docker run -d --name kirocrew \
  -p 127.0.0.1:5476:5476 \
  -v kirocrew-home:/home/kirocrew \
  ghcr.io/kirodotdev/kirocrew:stable

Or with compose: copy docker/compose.yaml and run docker compose up -d.

Images and tags

Tag Meaning
stable / latest Latest stable release (moves on each stable cut)
insider Latest insider pre-release
nightly Latest nightly build
0.1.0, 0.1.0-insider.4, 0.1.0-nightly.202607261234 Exact immutable versions

linux/amd64 and linux/arm64 are published under every tag. Version tags are never repointed once published; pin a version tag (or a digest) for reproducible deployments. See Release channels for who each channel is for. Every published manifest carries SLSA build provenance — verify with:

gh attestation verify oci://ghcr.io/kirodotdev/kirocrew:stable --repo kirodotdev/KiroCrew

First-run setup

Two one-time steps after the container is up:

  1. Log in the agent runtime (chat sessions run on kiro-cli):

    docker exec -it kirocrew kiro-cli login
    

    Credentials persist in the kirocrew-home volume, so login survives container upgrades.

  2. Open the dashboard — every request requires a token; mint a login link yourself:

    docker exec kirocrew kirocrew token --ttl 2h
    

    Open the printed link, substituting the host you reach the container on (with the mapping above: http://localhost:5476/?token=...). Login links expire minutes after minting — mint, then open immediately. (The gateway also prints one link at boot, as on every platform; by the time you read it in docker logs it has usually expired, so docker exec minting is the reliable path.)

Configuration

Channel credentials load from the environment (or from .env in the data home). Pass them with -e / compose environment::

Variable Purpose
SLACK_BOT_TOKEN, SLACK_APP_TOKEN, KIROCREW_OWNER_ID Slack bot (Socket Mode)
DISCORD_BOT_TOKEN Discord bot
TELEGRAM_BOT_TOKEN Telegram bot
WECOM_BOT_ID, WECOM_SECRET WeCom bot
WEIXIN_TOKEN Weixin channel
WEBEX_BOT_TOKEN Webex bot
MICROSOFT_APP_ID, MICROSOFT_APP_PASSWORD, MICROSOFT_APP_TENANT_ID Microsoft Teams bot (requires the teams extra)
FEISHU_APP_ID, FEISHU_APP_SECRET Feishu bot (requires the feishu extra)
KIRO_API_KEY kiro-cli model credential alternative to interactive login
KIROCREW_PORT Dashboard port (default 5476)
KIROCREW_BIND Bind address inside the container (image default 0.0.0.0; see below)
KIROCREW_ALLOW_UNSANDBOXED Set 1 to explicitly allow agent exec without the inner sandbox (see Sandbox below)

Credential hygiene: on every start the entrypoint moves the channel credentials it finds in the environment into the data home's .env file (mode 600) and removes them from the gateway's environment before the gateway starts — so they never sit in the long-lived gateway process's /proc/<pid>/environ. Environment values win over previously stored ones (same precedence the gateway itself applies), so changing a value in your compose .env and restarting updates the stored copy.

Four channels are unavailable in the stock image. Microsoft Teams needs PyJWT[crypto]==2.13.0 (the teams extra), Feishu needs lark-oapi>=1.4,<2 (the feishu extra), and WhatsApp needs neonize==0.4.3.post0 (the whatsapp extra). The Dockerfile installs the bare wheel, so use a custom image that installs the required distribution before enabling one of those transports. See the setup guidance in their packaged integration docs, including whatsapp-integration.md.

iMessage cannot be added to this image: it drives Messages.app on the machine the gateway runs on and needs macOS 14 or newer plus Full Disk Access and Automation grants, so no Linux container can serve it — imessage-integration.md.

Everything else lives in config.json inside the volume. Most settings are editable from the (token-authenticated) dashboard; the exceptions are the channel-credential pages (Slack, Discord, Telegram, WeCom, Weixin, Webex, Microsoft Teams, and Feishu) and secret-revealing views, which are read-only for any non-direct-local browser. The image ships no text editor, so edit those from the host — copy the file out, change it, copy it back, restart:

docker cp kirocrew:/home/kirocrew/.kiro/crew/config.json .
# edit config.json locally, then:
docker cp config.json kirocrew:/home/kirocrew/.kiro/crew/config.json
# docker cp writes the file root-owned; hand it back to the gateway user
# (uid 1000) or the dashboard can never save settings again:
docker exec -u 0 kirocrew chown kirocrew:kirocrew /home/kirocrew/.kiro/crew/config.json
docker restart kirocrew

(Or use environment variables / .env for the credential cases above, which need no file edit at all.)

State and upgrades

All persistent state — gateway home (~/.kiro/crew), kiro-cli credentials, agents, skills — lives under /home/kirocrew. One named volume covers all of it. Upgrade by pulling the newer image; state carries over:

docker compose pull && docker compose up -d

There is no in-container auto-update: the image is immutable and the tag is the version selector (channel tags track their channel; version tags pin).

Networking and security model

  • Why KIROCREW_BIND=0.0.0.0: outside Docker the gateway binds loopback only. Inside a container, published ports (-p) map to the container's bridge interface, so a loopback bind would be unreachable from the host. The image therefore binds all interfaces inside the container's network namespace — nothing is reachable from anywhere until you publish the port, and -p 127.0.0.1:5476:5476 keeps it host-local.
  • Auth surface, precisely: the API and WebSocket surface requires a valid dashboard token (cookie or minted link) regardless of bind address. Three deliberate carve-outs exist, none of which serve secrets:
    1. Liveness probes — /api/health, /api/live, /api/ready are tokenless AND exempt from the DNS-rebinding Host check (orchestrators address containers by IP). Their payloads are secret-free; the build identity fields are additionally stripped unless the caller is direct-local with a served Host.
    2. Static assets — the SPA shell, /assets/, /vendor/, and similar non-secret static files are served without a token (standard SPA bootstrap; the app is useless without a token once loaded).
    3. Local bootstrap — /api/token/local and /api/shutdown require a same-machine peer (a loopback address, or for the token endpoint the dashboard's kernel-verified unix socket) plus a filesystem secret, so they are unreachable through the published port by construction. CSRF origin checks apply to all state-changing requests, and the DNS-rebinding Host barrier applies to every request except the three probe paths.
  • Exposing beyond localhost: publishing 5476:5476 opens the TCP port, but LAN browsers will still be rejected until their origin is allowed — set dashboard.url (or KIROCREW_CORS_ORIGINS) to the address you browse from. The supported pattern is a TLS reverse proxy in front with dashboard.url set to its origin, exactly as for a non-container deployment.
  • Sandbox: on first run the entrypoint probes whether Kiro Crew's inner Linux user-namespace sandbox works under the container runtime's seccomp/AppArmor policy. If it does, it seeds agent.sandbox="auto" so agent commands run namespace-isolated from gateway state, same as a hardened native install. If no backend works, agent command execution stays DISABLED (fail-closed) — the gateway, dashboard, and channel bots run normally. To enable agents in that situation, either permit user namespaces (see Sandbox troubleshooting below) or restart with -e KIROCREW_ALLOW_UNSANDBOXED=1 to explicitly accept unsandboxed agent execution. In the consented posture the container is the only isolation boundary: treat its contents (mounted volumes included) as reachable by agent commands, and do not mount host paths you would not hand to the agent. The startup log states which posture was chosen.

Sandbox troubleshooting

Kiro Crew runs agent commands inside a Linux user-namespace sandbox that bind-mounts empty dirs over credential paths (~/.aws, ~/.ssh, etc.) so the agent subprocess cannot read gateway credentials. Building it takes three syscalls in order — unshare(CLONE_NEWUSER), unshare(CLONE_NEWNS), then a mount(MS_REC|MS_PRIVATE) on / inside the new mount namespace — and two different container guards can refuse them. Seccomp behavior varies by Docker and runtime version: modern defaults may permit the unshares, while hardened or RuntimeDefault profiles commonly return EPERM. A runtime's default AppArmor profile may instead block the mount (deny mount, errno 13 EACCES) after both unshares succeed. The startup probe performs all three steps, so either failure is reported as no usable backend and agent execution stays fail-closed until you choose a posture. Kubernetes commonly combines no explicit seccomp profile with a default AppArmor profile, making the mount the failing step — see Kubernetes and AppArmor.

How the startup probe decides your posture

On first run (no config.json in the volume) the entrypoint probes the sandbox and writes one of three postures:

Probe result Env var set? Posture written Agent execution
Sandbox works ✅ — sandbox=auto Namespace-isolated
No backend ❌ KIROCREW_ALLOW_UNSANDBOXED=1 sandbox_allow_unsandboxed_exec=true Allowed — container is the only boundary
No backend ❌ (not set) sandbox=auto (default) Disabled (fail-closed)

The entrypoint states which posture it seeded on the first run — the one where config.json does not yet exist. Each is emitted as one long line; wrapped here to read:

[entrypoint] First run: inner sandbox backend available — seeded
/home/kirocrew/.kiro/crew/config.json with agent.sandbox=auto so agent
subprocesses run namespace-isolated from gateway credentials.
[entrypoint] First run: NO inner sandbox backend under this runtime's seccomp
policy; KIROCREW_ALLOW_UNSANDBOXED=1 given — seeded
/home/kirocrew/.kiro/crew/config.json with sandbox=auto +
sandbox_allow_unsandboxed_exec=true. Agent subprocesses share the container user
and can read files owned by the gateway.
[entrypoint] First run: NO inner sandbox backend under this runtime's seccomp
policy. Seeded /home/kirocrew/.kiro/crew/config.json with sandbox=auto: agent
command execution is DISABLED (fail-closed) until you choose one of: (a) permit
user namespaces (--security-opt seccomp=<profile permitting unshare/clone>) and
restart to get the inner sandbox, or (b) restart with -e
KIROCREW_ALLOW_UNSANDBOXED=1 to explicitly accept unsandboxed agent execution
(the container is then the only isolation boundary).

A later run does not repeat the seeding line — the config already carries the posture. It prints the standing reminder instead, whenever agent.sandbox_allow_unsandboxed_exec is absent from config.json, naming the two ways to let agent commands through. See Check the startup log.

Option A — Kiro Crew seccomp profile (recommended)

The repo ships docker/seccomp/kirocrew-seccomp.json: the Docker default allow-list extended with unconditional unshare, clone, and mount rules. This is strictly less permissive than --security-opt seccomp=unconfined or --privileged — all other Docker default restrictions apply.

Image-only users (no repo checkout): download the profile directly:

curl -fsSL https://raw.githubusercontent.com/kirodotdev/KiroCrew/main/docker/seccomp/kirocrew-seccomp.json \
  -o kirocrew-seccomp.json

Then start the container:

docker run -d --name kirocrew \
  -p 127.0.0.1:5476:5476 \
  -v kirocrew-home:/home/kirocrew \
  --security-opt seccomp=kirocrew-seccomp.json \
  ghcr.io/kirodotdev/kirocrew:stable

Or in compose (add to the kirocrew service):

security_opt:
  - seccomp:./kirocrew-seccomp.json

If you run Compose from a repository checkout instead of using the downloaded file above, use seccomp:./docker/seccomp/kirocrew-seccomp.json.

With this profile the inner sandbox runs normally and credential directories are hidden from agent subprocesses inside the container.

On a host where AppArmor is enabled (Ubuntu and Debian families; check cat /sys/module/apparmor/parameters/enabled), the seccomp profile is only half the change: Docker also attaches its docker-default AppArmor profile, whose deny mount refuses the launcher's first mount with EACCES after both unshares succeeded. Add the AppArmor switch alongside the seccomp profile:

docker run -d --name kirocrew \
  -p 127.0.0.1:5476:5476 \
  -v kirocrew-home:/home/kirocrew \
  --security-opt apparmor=unconfined \
  --security-opt seccomp=kirocrew-seccomp.json \
  ghcr.io/kirodotdev/kirocrew:stable

apparmor=unconfined lifts only AppArmor's per-container rules; the seccomp profile, dropped capabilities and the non-root user still apply. No root or CAP_SYS_ADMIN is involved — inside the user namespace it creates, the launcher already holds the capabilities its own mount namespace needs.

Kubernetes and AppArmor

A Pod inverts Docker's defaults: Kubernetes applies no seccomp profile unless one is set (so both unshares succeed), and on AppArmor-enabled nodes the container runtime applies its default AppArmor profile (so the propagation mount is refused). The gateway then reports

mount(MS_REC|MS_PRIVATE) on / failed with errno 13 (EACCES)

with the remedy token mount_denied, and a kiro-cli that is installed and signed in stays unverified until you choose one of two postures. Neither needs a root user or CAP_SYS_ADMIN.

Let the sandbox run — set the container's AppArmor profile to Unconfined (Kubernetes 1.30+; earlier versions use the container.apparmor.security.beta.kubernetes.io/<container>: unconfined annotation):

spec:
  containers:
    - name: kirocrew
      securityContext:
        appArmorProfile:
          type: Unconfined

If your cluster also enforces seccompProfile: RuntimeDefault, that profile blocks unshare; load kirocrew-seccomp.json on the node and reference it with seccompProfile: {type: Localhost, localhostProfile: <path>}.

Accept the container as the only boundary — when the AppArmor profile cannot change, make the sandbox's absence clean rather than partial and opt in explicitly (Option B's posture):

      securityContext:
        seccompProfile:
          type: RuntimeDefault

together with "agent": {"sandbox_allow_unsandboxed_exec": true} in the container's config.json (or KIROCREW_ALLOW_UNSANDBOXED=1 on first run). RuntimeDefault makes the very first probe step fail, so the gateway takes its documented no-backend path and the opt-in applies to every spawn; the mount_denied verdict alone already routes there, so the seccomp line is a hardening step, not a requirement. In this posture agent subprocesses share the container user and can read files owned by the gateway.

An enterprise sandbox.min_level policy overrides the opt-in on a governed host; such a host runs no agent subprocess until its sandbox works.

Option B — Explicit unsandboxed consent

If you cannot modify the seccomp policy (managed Kubernetes, locked-down runtime, Docker Desktop with restricted settings):

docker run -d --name kirocrew \
  -p 127.0.0.1:5476:5476 \
  -v kirocrew-home:/home/kirocrew \
  -e KIROCREW_ALLOW_UNSANDBOXED=1 \
  ghcr.io/kirodotdev/kirocrew:stable

In this posture the container is the only isolation boundary. Do not mount host paths you would not hand directly to the agent.

Option C — --privileged (not recommended)

--privileged disables all seccomp, AppArmor, and capability restrictions. It does let the inner sandbox work, but the cost — full host device access and all capabilities inside the container — is disproportionate. Prefer Option A.

WSL2 + Docker CE (non-Desktop)

Running Docker CE natively inside WSL2 without Docker Desktop can hit the same EPERM even with Option A, because the WSL2 kernel itself may have user-namespace support disabled (CONFIG_USER_NS=n in the WSL2 kernel config).

Check from inside a running container:

docker exec kirocrew python3 -c \
  "from kiro_crew.sandbox import userns_available; print(userns_available())"
  • True → user namespaces work on the kernel; re-check your seccomp profile (Option A).
  • False → the WSL2 kernel lacks user-namespace support; use Option B, or switch to Docker Desktop which ships a kernel with CONFIG_USER_NS=y.

Verifying your posture

# Check which posture was chosen at startup
docker logs kirocrew | grep '\[entrypoint\]'

# Live check from inside the container
docker exec kirocrew python3 -c \
  "from kiro_crew.sandbox import detect_backend; print(detect_backend())"
# Expected: "namespace" (inner sandbox active) or "none" (unsandboxed)

Health

The image ships a HEALTHCHECK against /api/health. Orchestrators can use /api/live and /api/ready for liveness/readiness probes; all three are token-free and secret-free.

Building locally

The image consumes a built wheel (never the raw source tree), keeping Docker bytes identical to pip bytes for a given version:

make wheel                                   # builds dist/kirocrew-*.whl
docker build -f docker/Dockerfile -t kirocrew:dev .

The Dockerfile consumes the wheel through a BuildKit bind mount, so the build requires BuildKit — the default builder since Docker 23. On an older engine (where the classic builder rejects RUN --mount with Unknown flag: mount), prefix the build with DOCKER_BUILDKIT=1.

Troubleshooting

For solutions to common Docker deployment issues — port binding, permission errors, sandbox failures, health check loops, data loss, and more — see the dedicated Docker Troubleshooting Guide.