SSD Nodes Learn 🎉 VPS from $4.99/mo
Guides Matt ConnorBy Matt Connor

Self-host KiroCrew on a VPS: always-on agent

Run KiroCrew as a pinned container on your own VPS so memory and schedules survive reboots. Docker, systemd, SSH access, backups and rollback.

Why self-host KiroCrew on a VPS instead of a laptop

Self-hosting KiroCrew only pays off on a machine that never sleeps, so a VPS is the right home for it and a laptop is not. KiroCrew keeps session history, semantic memory, scheduled jobs and the approval queue on disk, and it reloads all of that when the process restarts. None of it helps if the process is not running at 03:00 when a scheduled job is due, and a closed laptop is not running it.

KiroCrew is an open source agent workspace from the Kiro team, licensed Apache 2.0, with its first public releases landing in early August 2026. One process, called the gateway, owns the state and serves a web dashboard on port 5476. You reach that gateway from the dashboard, from the kirocrew CLI, or from a chat channel such as Slack. The gateway is the only thing you are self-hosting, so this guide is about keeping it alive, keeping it off the public internet, and being able to put it back after a bad upgrade.

Two things to know before you start. KiroCrew drives kiro-cli, which needs a one-time sign-in with a Kiro account, and agent inference is billed to a Kiro plan, so as of August 2026 this is not an offline setup. The project is also only weeks old. Assume you will need to roll back at some point, and install it in a way that lets you. If you have not run an agent on a server before, running a coding agent on a VPS covers the ground rules this guide builds on.

What KiroCrew needs, and where its state lives

A native install needs Python 3.10 or newer (the project recommends 3.12), Node.js 18 or newer if you build the dashboard from source, and kiro-cli, which the first launch installs and signs in for you. The container install needs none of that on the host. It needs Docker. That is the main reason to prefer it.

State lives in ~/.kiro/crew, and the KIROCREW_HOME environment variable moves it elsewhere. What sits inside:

  • config.json: gateway settings and chat channel credentials.
  • .env: secrets.
  • workspace/memory/: preferences, project notes and chat history.
  • memory.db and memory_index.db: the semantic and full-text indexes.
  • models/: the embedding model, downloaded on first run.
  • gateway.log and security_events.jsonl: the runtime log and the security event log.

That directory is the install. Copy it to a new VPS and you have moved your agent, which is why the backup section below matters more than the install section.

Plan for disk rather than RAM. The gateway is a Python process; what actually loads the box is whatever the agent runs, a build or a test suite. The state directory grows with chat history, and the embedding model arrives on first start, so measure it on your own box with du -sh ~/.kiro/crew after a few weeks instead of trusting any figure published in the first month of a project.

Which of the three install paths should you use

The project publishes three. The one-line installer fetches a wheel and puts kirocrew on your PATH:

curl -fsSL https://download.crew.kiro.dev/cli.sh | sh

It takes a channel flag and a version flag:

curl -fsSL https://download.crew.kiro.dev/cli.sh | sh -s -- --channel insider
curl -fsSL https://download.crew.kiro.dev/cli.sh | sh -s -- --version 0.1.3

The container image is published at ghcr.io/kirodotdev/kirocrew, for linux/amd64 and linux/arm64 under every tag. The source build is git clone plus make build, and it is for people changing the code, not for people running it.

Use the container. A native install puts Python packages, Node and kiro-cli on the same host that runs your other services, so an upgrade that goes wrong leaves you unpicking that by hand. The container keeps the runtime in one image and the state in one volume, which makes a rollback a tag change and a restart.

Pin the image to a release tag, not to stable

The project's own example uses the stable tag:

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

stable is a moving tag. It points at whatever the newest stable release happens to be, so the next pull can change the version you are running without you choosing it, and the tag records nothing about which version that was. Version tags are immutable, so pin one. The newest release as of 6 August 2026 is 0.1.3, published on 5 August 2026. There is also a nightly tag, which on a project this young means the code changed this morning.

Write /opt/kirocrew/compose.yaml:

services:
  kirocrew:
    image: ghcr.io/kirodotdev/kirocrew:0.1.3
    container_name: kirocrew
    restart: unless-stopped
    ports:
      - "127.0.0.1:5476:5476"
    volumes:
      - kirocrew-home:/home/kirocrew

volumes:
  kirocrew-home:

Start it, then check the health endpoint the image also uses for its own HEALTHCHECK:

cd /opt/kirocrew
docker compose up -d
docker compose ps
curl -s http://127.0.0.1:5476/api/health

docker compose ps should report the container as healthy within a minute or so, and /api/health answers without a token (as do /api/live and /api/ready, which is what makes them usable as probes). If the state stays at starting, read docker logs kirocrew before changing anything. First run downloads the embedding model, so a slow link makes the first start long.

Keep it running with systemd

restart: unless-stopped brings the container back after a crash and after a reboot, as long as Docker itself starts at boot. A unit file makes that dependency explicit and gives you one command that stops the whole stack before a backup. Starting a Docker Compose stack on boot covers the general pattern. This is the KiroCrew shape of it, in /etc/systemd/system/kirocrew.service:

[Unit]
Description=KiroCrew gateway
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/kirocrew
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now kirocrew
systemctl status kirocrew

systemctl status kirocrew should read active (exited), which is the healthy result for this unit. Type=oneshot with RemainAfterExit=yes is right here because docker compose up -d returns as soon as the container is started: systemd is tracking the fact that the stack is up, not a foreground process. Write Type=simple instead and systemd sees the command exit immediately, marks the service dead, and then either gives up or restart-loops depending on your Restart= setting. For a native install the project ships its own equivalent, kirocrew service install, which writes /etc/systemd/system/kirocrew.service and runs the gateway as your user. Do not run both units. The wider version of this topic is in systemd services and timers on a VPS.

First run: sign in and get a dashboard token

The container starts the gateway, but the agent runtime is not signed in yet. Sign in inside the container:

docker exec -it kirocrew kiro-cli login

That prints a device code and a URL you open in your own browser. Then mint a dashboard token:

docker exec kirocrew kirocrew token --ttl 2h

The dashboard URL is http://localhost:5476/?token=<the token>. Tokens expire: sessions default to one hour and the documented maximum is twenty hours. A dashboard that loads blank or bounces you straight back out is usually an expired token, so mint another. Never paste a token into a ticket or a chat message, because whoever holds it holds your agent.

Reach the dashboard over SSH, and never publish port 5476

Look again at the bind address in the project's example: -p 127.0.0.1:5476:5476. Inside the container the gateway listens on 0.0.0.0, because it has to be reachable through the port mapping, but the mapping itself publishes only to loopback on the host. Delete the 127.0.0.1: prefix and the gateway is on the public internet for anyone who scans that port. A firewall rule will not save you either: Docker publishes ports by writing DNAT rules that are evaluated before ufw's filtering, so ufw deny 5476 does nothing to a published port. Docker ports bypassing ufw walks through that mechanism.

Forward the port over SSH from your laptop instead:

ssh -N -L 5476:127.0.0.1:5476 you@your-server.example.com

Leave that running and open http://localhost:5476/?token=<the token> locally. To make the forward automatic on every connection, put it in ~/.ssh/config:

Host your-server.example.com
    LocalForward 5476 127.0.0.1:5476

If port 5476 is already in use on your laptop, change the left-hand number only: ssh -N -L 45476:127.0.0.1:5476 you@your-server.example.com, then browse to http://localhost:45476/?token=....

One documented behaviour to expect over a tunnel: the gateway reads forwarded requests as remote, so config-write and secret-reveal endpoints in the dashboard refuse them. A settings change that will not save over SSH is this, not a bug. Edit the config on the host instead:

docker cp kirocrew:/home/kirocrew/.kiro/crew/config.json .
# edit config.json here
docker cp config.json kirocrew:/home/kirocrew/.kiro/crew/config.json
docker exec -u 0 kirocrew chown kirocrew:kirocrew /home/kirocrew/.kiro/crew/config.json
docker restart kirocrew

For phone access the project points at Tailscale's tailscale serve, which keeps the dashboard inside your own tailnet rather than on a public hostname. Prefer that to a public reverse proxy. The token travels in the URL, and a URL is written into every access log on its path.

Give the agent the smallest blast radius you can

The container probes for sandbox support on first start, and the result decides whether agents can execute anything. If namespace isolation is available, agent subprocesses run isolated. If it is not available and KIROCREW_ALLOW_UNSANDBOXED=1 is not set, execution is refused rather than run unconfined, so a gateway that looks healthy while every task stalls is usually this. The decision is in docker logs kirocrew from that first run. The project also publishes a seccomp (secure computing mode) profile you can apply:

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

If you do set KIROCREW_ALLOW_UNSANDBOXED=1, be clear about what changed: the container is now the only boundary between the agent and your server. The project's warning is worth repeating in full. Do not mount host paths you would not hand directly to the agent. In practice that rules out the Docker socket, any bind mount of /, and any directory holding another service's data.

The rest is the frame that applies to every agent allowed to run commands. Scope its credentials to the one repository or the one bucket it needs, never a personal token with account-wide rights. Run it as a dedicated user whose home holds nothing else, which is what least privilege users on a VPS is for. When the agent writes code and then runs that code, give it a machine it is allowed to break: a disposable VM for coding agents is a stronger boundary than any flag in this compose file, because you delete it instead of cleaning it. The same reasoning shapes running OpenClaw safely on a VPS and self-hosting the Hermes agent on a VPS. Scheduled work also spends money while you sleep, since inference bills to your Kiro plan, so set the limits described in controlling what an AI agent costs on a VPS before you add a nightly job.

Back up the state volume before every upgrade

Find the real volume name first. Compose prefixes named volumes with the project name, which defaults to the directory name, so the volume declared as kirocrew-home in /opt/kirocrew/compose.yaml is created as kirocrew_kirocrew-home:

docker volume ls

Stop the gateway before copying anything. memory.db and memory_index.db are SQLite databases, and copying a database while it is being written can capture a half-written transaction, which restores as a corrupt file. The project's own migration instructions say the same: move memory only while the gateways are stopped.

sudo systemctl stop kirocrew
docker run --rm -v kirocrew_kirocrew-home:/data:ro -v "$PWD":/backup \
  alpine tar czf /backup/kirocrew-2026-08-06.tgz -C /data .
sudo systemctl start kirocrew

Copy the archive off the box. Restoring is the same command with the container stopped and tar xzf in place of tar czf:

sudo systemctl stop kirocrew
docker run --rm -v kirocrew_kirocrew-home:/data -v "$PWD":/backup \
  alpine tar xzf /backup/kirocrew-2026-08-06.tgz -C /data
sudo systemctl start kirocrew

Moving to a new host is a different job from restoring in place, and the project is specific about it. Chat history and project notes under workspace/memory/ carry over, and so do the two database files and config.json. PID files, the security event log and .env are tied to the old host, so leave them behind and enter the secrets again on the new box.

How do you roll back a bad upgrade

Upgrading is short, and it is only safe because you pinned a version. Take the backup first, then change the tag:

sudo systemctl stop kirocrew
# take the backup here, as above
sudo nano /opt/kirocrew/compose.yaml   # set the new image tag
sudo systemctl start kirocrew
docker compose -f /opt/kirocrew/compose.yaml ps
curl -s http://127.0.0.1:5476/api/health

docker compose up -d pulls the image if it is not already on the box, so the tag edit is the whole upgrade. Rolling back is the same sequence with the old number, and it gives you the exact image you had before, because version tags are immutable.

The binary rolls back cleanly. The state is the part that might not. A newer gateway can rewrite config.json or migrate the memory databases into a shape an older gateway does not read, and no downgrade path is documented as of August 2026. So if the older image starts and then behaves oddly, do not debug it. Stop it, restore the backup you took before the upgrade, and start again. That is the entire reason the backup comes first, and it is why the habit of upgrading now and backing up later fails on a project this young.

What is not proven here

Be honest about the age of this software. Version 0.1.3 is days old at the time of writing, its release notes are automated changelog links rather than migration notes, and there is no track record of upgrades yet. Nothing in this guide is a long-run result, so treat memory growth, database size and scheduler reliability as things to measure on your own box rather than things to assume.

Two behaviours are worth testing yourself before you depend on them. First, whether a downgrade reads state written by a newer version: try it on a copy of the volume while it does not matter, not during an outage. Second, what the gateway does when the Kiro sign-in expires while a scheduled job is due. Both are the kind of rough edge a young project files down quietly between releases, and both are cheap to check now.

FAQ

Why does the KiroCrew dashboard not open on my server's public IP?

Because the published example binds the port to loopback. -p 127.0.0.1:5476:5476 maps the container's port to the host's loopback address only, which is deliberate. Reach it by forwarding the port over SSH with ssh -N -L 5476:127.0.0.1:5476 you@your-server, then open http://localhost:5476/?token=<token> on your laptop. Removing the 127.0.0.1: prefix to make it reachable puts the gateway on the public internet, and a firewall rule will not contain it, because Docker's published-port DNAT rules are evaluated before ufw filters the traffic.

Where does KiroCrew store its data, and what should I back up?

Everything sits under ~/.kiro/crew, which is /home/kirocrew/.kiro/crew inside the container image, and KIROCREW_HOME relocates it. Back up the whole directory, or the whole Docker volume, with the gateway stopped. memory.db and memory_index.db are SQLite databases, so a copy taken while the gateway is writing can be inconsistent. When moving to a new host, workspace/memory/, the two database files and config.json carry across, while PID files, the security event log and .env belong to the old host.

Should I use the stable tag or a version tag?

Use a version tag. stable moves every time a release ships, so the version you run can change under you on the next pull, and the tag itself tells you nothing about what is running. Version tags such as 0.1.3 are immutable, and that is exactly what makes a rollback work: you put the old number back and get the identical image. As of 6 August 2026 the newest release is 0.1.3.

Why does my agent refuse to run any commands?

The container probes for sandbox support on its first start. If it cannot isolate agent subprocesses and KIROCREW_ALLOW_UNSANDBOXED=1 is not set, it refuses to execute them rather than running them unconfined, so the gateway looks healthy while every task stalls. docker logs kirocrew shows the sandbox decision from that first run. Setting the variable makes the container the only boundary between the agent and the host, so if you set it, mount nothing you would not hand to the agent directly.

Do I need a Kiro account to self-host KiroCrew?

Yes, as of August 2026. KiroCrew is free software under Apache 2.0, but it drives kiro-cli, which requires a one-time sign-in, and agent inference is billed to a Kiro plan. In the container, run docker exec -it kirocrew kiro-cli login and approve the device code in your browser. Until that sign-in completes, the gateway starts and the dashboard loads, but the agent has no model to talk to.