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.
| 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
Two one-time steps after the container is up:
-
Log in the agent runtime (chat sessions run on kiro-cli):
docker exec -it kirocrew kiro-cli loginCredentials persist in the
kirocrew-homevolume, so login survives container upgrades. -
Open the dashboard — every request requires a token; mint a login link yourself:
docker exec kirocrew kirocrew token --ttl 2hOpen 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 indocker logsit has usually expired, sodocker execminting is the reliable path.)
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.)
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).
- 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:5476keeps 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:
- Liveness probes —
/api/health,/api/live,/api/readyare 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. - 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). - Local bootstrap —
/api/token/localand/api/shutdownrequire 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.
- Liveness probes —
- Exposing beyond localhost: publishing
5476:5476opens the TCP port, but LAN browsers will still be rejected until their origin is allowed — setdashboard.url(orKIROCREW_CORS_ORIGINS) to the address you browse from. The supported pattern is a TLS reverse proxy in front withdashboard.urlset 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=1to 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.
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.
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.
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.jsonThen 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:stableOr in compose (add to the kirocrew service):
security_opt:
- seccomp:./kirocrew-seccomp.jsonIf 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:stableapparmor=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.
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: UnconfinedIf 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: RuntimeDefaulttogether 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.
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:stableIn this posture the container is the only isolation boundary. Do not mount host paths you would not hand directly to the agent.
--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.
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 withCONFIG_USER_NS=y.
# 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)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.
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.
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.