Skip to content

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 ​

TargetGood forNotes
node apps/server/dist/bin.jsDevelopment, single-user desktopNo service supervision; use systemd/launchd for uptime.
DockerReproducible single nodeImage includes git and curl for the shell tool.
ComposeLocal team instanceToken required, workspace and data mounted.
KubernetesShared internal deploymentSingle replica today; see scaling.
HelmKubernetes with per-environment valuesChart mirrors the kustomize base; helm lint runs in CI.
systemdLinux serversHardened unit included.
launchdmacOSPer-user agent, logs under ~/Library or a shared path.
Scheduled TaskWindowsIncluded 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.com

To 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 \
  kestrel

The 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 kestrel

docker 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/k8s

What the base sets:

  • runAsNonRoot, uid/gid 1000, seccompProfile: RuntimeDefault.
  • readOnlyRootFilesystem with emptyDir mounts for /tmp.
  • Separate PVCs for /data (transcripts) and /workspace (the project).
  • Liveness and readiness probes against /api/health.
  • Recreate strategy, because both PVCs are ReadWriteOnce and 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.com

The 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 -f

The 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.plist

The 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).Guid

It 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 tokens

Each 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 ​

  1. Build the new image or copy the new build.
  2. Stop the old process (SIGTERM; it cancels runs and disposes plugins).
  3. Start the new one with the same /data and 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=json for collectors.
  • Health: GET /api/health includes uptime, plugin count and session count.
  • Bootstrap: GET /api/bootstrap is a cheap way to assert models, tools and plugins after deploy.
  • SSE: /api/events is 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 ​

WorkloadCPUMemory
One developer, small repo0.25 core256 MB
Several sessions, large repo1 core1 GB
Shell-heavy builds2 cores2 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.

Released under the MIT License.