Appearance
Deployment
Kestrel is one Node process with two ports of interest: the HTTP API and the SSE stream on the same listener. Any platform that can run a container or a Node 22 service can host it. This page covers the shapes we test and the operational details that are easy to get wrong.
Choosing a target
| Target | Good for | Notes |
|---|---|---|
node apps/server/dist/bin.js | Development, single-user desktop | No service supervision; use systemd/launchd for uptime. |
| Docker | Reproducible single node | Image includes git and curl for the shell tool. |
| Compose | Local team instance | Token required, workspace and data mounted. |
| Kubernetes | Shared internal deployment | Single replica today; see scaling. |
| Helm | Kubernetes with per-environment values | Chart mirrors the kustomize base; helm lint runs in CI. |
| systemd | Linux servers | Hardened unit included. |
| launchd | macOS | Per-user agent, logs under ~/Library or a shared path. |
| Scheduled Task | Windows | Included PowerShell installer. |
Docker
The release workflow publishes a signed multi-architecture image:
bash
export KESTREL_IMAGE=ghcr.io/rakuen-ly/kestrel:0.2.1
docker run --rm -p 127.0.0.1:7331:7331 \
-e KESTREL_TOKEN="$(openssl rand -hex 24)" \
-e OPENAI_API_KEY=sk-... \
-v "$PWD/workspace:/workspace" \
-v kestrel-data:/data \
"$KESTREL_IMAGE"Tags are 0.2.1, the floating 0.2, latest and sha-<commit>; the image covers linux/amd64 and linux/arm64. Pin a version or a digest for anything long-lived — latest moves. Every tag is signed with cosign keyless signing, so provenance is checkable before you run it:
bash
cosign verify ghcr.io/rakuen-ly/kestrel:0.2.1 \
--certificate-identity-regexp \
'https://github.com/Rakuen-LY/kestrel/.github/workflows/release.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comTo build from a checkout instead, docker build -t kestrel . and substitute kestrel for "$KESTREL_IMAGE" in the commands on this page.
The image runs as the unprivileged node user (uid 1000). /workspace is where tools operate; /data holds transcripts, accounts and per-tenant workspaces. Mount a project directory, never the Docker socket. node --version tells you which runtime the image ships; /api/health reports the same version as the tag, so a mismatch is visible immediately.
Several users in one container
Add accounts mode and bootstrap the first administrator on the first start:
bash
docker run --rm -p 127.0.0.1:7331:7331 \
-e KESTREL_AUTH_MODE=accounts \
-e KESTREL_BOOTSTRAP_TOKEN="$(openssl rand -hex 32)" \
-e OPENAI_API_KEY=sk-... \
-v kestrel-data:/data \
kestrelThe bootstrap token is only used while no user exists. Create further users with docker exec -it <container> kestrel users create <name> --role member --tenant <id>, or over the API. Tenants get their own workspace under /data/tenants/<id>/workspace unless auth.workspaces maps them elsewhere. See Authentication and tenants.
Compose
bash
cp .env.example .env
# edit KESTREL_TOKEN and provider keys
mkdir -p workspace
docker compose up -d # pulls the published image
docker compose logs -f kestreldocker compose up -d --build builds this checkout instead of pulling, tagging the result with the same name; KESTREL_IMAGE=ghcr.io/rakuen-ly/kestrel:0.2.1 pins a different tag or digest. Compose binds to 127.0.0.1 by default. Put a TLS-terminating proxy in front for remote access.
Kubernetes
The manifests in deploy/k8s are a kustomize base:
bash
kubectl create namespace kestrel
kubectl -n kestrel create secret generic kestrel-secrets \
--from-literal=KESTREL_TOKEN="$(openssl rand -hex 24)" \
--from-literal=KESTREL_BOOTSTRAP_TOKEN="$(openssl rand -hex 32)" \
--from-literal=OPENAI_API_KEY=sk-...
kubectl apply -k deploy/k8sWhat the base sets:
runAsNonRoot, uid/gid 1000,seccompProfile: RuntimeDefault.readOnlyRootFilesystemwith emptyDir mounts for/tmp.- Separate PVCs for
/data(transcripts) and/workspace(the project). - Liveness and readiness probes against
/api/health. Recreatestrategy, because both PVCs areReadWriteOnceand the in-memory event buffer is process-local.- Ingress annotations that disable proxy buffering and extend timeouts for SSE.
Helm
deploy/helm/kestrel wraps the same objects as the kustomize base and exposes every setting as a value:
bash
helm install kestrel deploy/helm/kestrel \
--namespace kestrel --create-namespace \
--set secret.create=true \
--set secret.token="$(openssl rand -hex 24)" \
--set ingress.host=kestrel.example.comThe chart is also packaged and attached to every release, so a cluster can install a pinned version without the checkout:
bash
helm install kestrel \
https://github.com/Rakuen-LY/kestrel/releases/download/v0.2.1/kestrel-0.2.1.tgz \
--namespace kestrel --create-namespace \
--set secret.create=true --set secret.token="$(openssl rand -hex 24)"By default the chart expects the kestrel-secrets Secret you created above (secret.existingSecret) and renders none of its own; secret.create=true inverts that and requires secret.token. config is the entire kestrel.config.json, persistence.* sizes the two claims, and image.tag defaults to the chart's appVersion, so a chart release and its image move together. CI runs helm lint and validates the rendered output with kubeconform on every change.
Scaling
The current architecture assumes a single writer:
- sessions and the event replay buffer live in process memory;
- the file store is not safe for concurrent writers across pods;
- approvals are held in memory.
Run one replica. If you need more, the path is a shared SessionStore (Postgres/Redis), a shared event bus, and sticky routing for SSE. Those are roadmap items, not configuration flags.
systemd
bash
sudo useradd --system --home /opt/kestrel --shell /usr/sbin/nologin kestrel
sudo mkdir -p /opt/kestrel /etc/kestrel /var/lib/kestrel /srv/workspace
sudo chown -R kestrel:kestrel /opt/kestrel /var/lib/kestrel /srv/workspace
# copy the built repository to /opt/kestrel
sudo cp deploy/systemd/kestrel.service /etc/systemd/system/
sudo cp deploy/systemd/kestrel.env.example /etc/kestrel/kestrel.env
sudo chmod 600 /etc/kestrel/kestrel.env
sudo systemctl daemon-reload
sudo systemctl enable --now kestrel
journalctl -u kestrel -fThe unit uses ProtectSystem=strict and allows writes only to /var/lib/kestrel and /srv/workspace. Add your own workspace path to ReadWritePaths if it differs.
macOS (launchd)
bash
sudo cp -R . /opt/kestrel
sudo mkdir -p /Users/Shared/kestrel-workspace /Users/Shared/kestrel-data
sudo cp deploy/launchd/dev.kestrel.agent.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/dev.kestrel.agent.plistThe example plist does not inject secrets. Put them in the plist's EnvironmentVariables (readable by the user) or start through a wrapper that reads a protected file. Logs go to /Users/Shared/kestrel-data/.
Windows
Run deploy/windows/install-task.ps1 from an elevated PowerShell prompt after copying the built repository to C:\kestrel:
powershell
.\install-task.ps1 -Workspace 'C:\src\my-app' -Token (New-Guid).GuidIt registers a scheduled task at logon, writes a launcher and an environment file under C:\ProgramData\kestrel, and starts the server. For a true Windows service, wrap the same command with NSSM or a similar service host.
Reverse proxy
SSE needs three things: no response buffering, a long read timeout, and HTTP/1.1 keep-alive.
nginx
location / {
proxy_pass http://127.0.0.1:7331;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}If the console shows repeated reconnects, buffering is the usual cause. Caddy works without extra configuration; Traefik needs buffering disabled on the service.
The server binds to loopback by default. Keep it that way when a proxy is in front, and require a credential even then: KESTREL_TOKEN, or auth.mode: "accounts" with per-user tokens.
Persistence and backups
/data/sessions/<id>/session.json # default tenant
/data/tenants/<tenantId>/sessions/<id>/... # other tenants
/data/auth/accounts.json # users, tenants, hashed tokensEach session is a single JSON document written atomically. Back up the whole directory; restore it before starting the process. A newer build ignores unknown fields and fills missing optional ones, so downgrades are not guaranteed. There is no migration tool yet.
Memory notes live inside the workspace by default (.kestrel/memory.json), so back them up with the project if they matter.
Upgrades
- Build the new image or copy the new build.
- Stop the old process (SIGTERM; it cancels runs and disposes plugins).
- Start the new one with the same
/dataand workspace mounts.
Sessions are forward-compatible within the same protocol major. A protocol change that removes fields would require a migration; /api/health reports the protocol version so orchestrators can gate a rollout.
Observability
- Logs: structured lines;
KESTREL_LOG_FORMAT=jsonfor collectors. - Health:
GET /api/healthincludes uptime, plugin count and session count. - Bootstrap:
GET /api/bootstrapis a cheap way to assert models, tools and plugins after deploy. - SSE:
/api/eventsis an operator firehose; do not expose it publicly.
Metrics and tracing are not built in yet. The kernel's service ownership makes instrumentation straightforward, but it is on the roadmap rather than in this release.
Resource guidance
| Workload | CPU | Memory |
|---|---|---|
| One developer, small repo | 0.25 core | 256 MB |
| Several sessions, large repo | 1 core | 1 GB |
| Shell-heavy builds | 2 cores | 2 GB |
The agent itself is I/O-bound; the shell tool inherits whatever the container allows. Set pod or container limits so a runaway build cannot starve the host.