KB LabsDocs
Эта страница ещё не переведена на русский.Помочь с переводом на GitHub →

kb-deploy

Обновлено 31 мая 2026 г.


Deploy Docker services to VPS over SSH, plus declarative fleet rollout of the KB Labs platform with config delivery, canary waves, and auto-rollback.

kb-deploy is a standalone Go binary with two delivery models:

  • Imperative run — builds Docker images, pushes them to a registry, and deploys over SSH using docker compose. It detects which services are affected by the latest git commit and deploys only those. This is the section most of this page covers.
  • Declarative apply — rolls out the KB Labs platform itself (gateway, rest-api, workflow, state) across a fleet of hosts from a single deploy.yaml, delivering each host's runtime config and resolving secrets along the way. See Declarative apply.

It also manages stateful infrastructure (databases, caches, queues) as a separate concern from application deployments — infra is never touched by kb-deploy run.

Install

Bash
curl -fsSL https://kblabs.ru/kb-deploy/install.sh | sh

Or download a binary directly from GitHub Releases.

Build from source:

Bash
git clone https://github.com/KirillBaranov/kb-labs
cd kb-labs/tools/kb-deploy && make build

Quick start

Bash
# Deploy targets affected by the last commit
kb-deploy run
 
# Deploy all targets regardless of git changes
kb-deploy run --all
 
# Deploy a specific target
kb-deploy run kb-labs-web
 
# Bring up all infrastructure services (idempotent)
kb-deploy infra up
 
# Check what's running (targets + infra)
kb-deploy status
kb-deploy infra status

Configuration

kb-deploy discovers config by walking up from cwd looking for .kb/deploy.yaml. Pass an explicit path with --config.

Environment variables

All string fields support ${VAR} substitution. Variables are resolved in priority order:

  1. Process environment — shell env, CI secrets.
  2. .env file in the repo root — loaded automatically, supports multiline quoted values.

Process env always wins over .env.

deploy.yaml reference

YAML
registry: ghcr.io/yourname
 
# ─── Infrastructure ──────────────────────────────────────────────────────────
# Stateful services managed independently from application targets.
# Never touched by kb-deploy run.
infrastructure:
  qdrant:
    type: docker-image
    image: qdrant/qdrant:latest
    ssh:
      host: ${SSH_HOST}
      user: ${SSH_USER}
      key_env: SSH_KEY       # name of the env var holding the private key PEM
    volumes:
      - qdrant_data:/qdrant/storage
    ports:
      - "6333:6333"
    restart: unless-stopped
    strategy: manual         # "manual" (default) | "diff"
 
  redis:
    type: docker-image
    image: redis:7-alpine
    ssh:
      host: ${SSH_HOST}
      user: ${SSH_USER}
      key_env: SSH_KEY
    volumes:
      - redis_data:/data
    restart: unless-stopped
    strategy: manual
 
# ─── Application targets ─────────────────────────────────────────────────────
# Stateless services deployed by kb-deploy run.
targets:
  kb-labs-web:
    watch:
      - sites/kb-labs-web/**  # paths that trigger a deploy when changed
    image: kb-labs-web         # image name (registry prefix added automatically)
    dockerfile: sites/kb-labs-web/apps/web/Dockerfile
    context: sites/kb-labs-web
    ssh:
      host: ${SSH_HOST}
      user: ${SSH_USER}
      key_env: SSH_KEY
    remote:
      compose_file: ~/app/docker-compose.yml
      service: web

Infrastructure strategy

StrategyBehavior
manual (default)Only kb-deploy infra up/down touches this service. run never does.
diffinfra up is called during run if the image digest changed. For services you want auto-updated in CI.

SSH key setup

Store the private key PEM in an environment variable (e.g. SSH_KEY). For CI, add it as a secret. The .env file supports multiline quoted values for local development:

Bash
# .env
SSH_HOST=1.2.3.4
SSH_USER=deploy
SSH_KEY="-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAA...
-----END OPENSSH PRIVATE KEY-----"

Never commit .env to git. Add it to .gitignore.

Commands

kb-deploy run

Build, push, and deploy affected or specified targets.

Bash
kb-deploy run                 # affected by last commit (git diff HEAD~1)
kb-deploy run --all           # all targets
kb-deploy run kb-labs-web     # specific target
 
kb-deploy run --json          # machine-readable output

What it does for each target:

  1. docker build the image with the current git SHA as tag
  2. docker push to the registry
  3. SSH into the host, docker pull the new image
  4. docker compose up -d to restart the service

kb-deploy infra up

Bring up infrastructure services. Idempotent — safe to run repeatedly.

Bash
kb-deploy infra up            # all infra services
kb-deploy infra up qdrant     # specific service
 
kb-deploy infra up --json

Behavior per service:

  • Already running — no-op
  • Stoppeddocker start
  • Absentdocker pull then docker run with configured volumes, ports, and restart policy

kb-deploy infra down

Stop and remove an infrastructure container.

Bash
kb-deploy infra down qdrant
kb-deploy infra down qdrant --json

This runs docker stop + docker rm. Data in named volumes is preserved.

kb-deploy infra status

Show runtime state of all infrastructure services.

Bash
kb-deploy infra status
kb-deploy infra status --json
● qdrant               running   qdrant/qdrant:latest
● redis                running   redis:7-alpine

kb-deploy status

Show last deployed SHA and timestamp per application target.

Bash
kb-deploy status
kb-deploy status --json

kb-deploy list

List configured application targets.

Bash
kb-deploy list
kb-deploy list --json

JSON contracts

All commands support --json, --agent (the compact variant — drops meta), and --output human|json|agent. Stdout contains exactly one JSON object. Exit code 0 means the command ran (not that all services are healthy). Exit 1 means a tool-level error (config not found, missing env var).

JSON
// kb-deploy run --json
{ "ok": true, "sha": "abc1234", "results": [
  { "target": "kb-labs-web", "sha": "abc1234", "ok": true }
]}
 
// kb-deploy infra up --json
{ "ok": true, "results": [
  { "name": "qdrant", "ok": true },
  { "name": "redis",  "ok": true }
]}
 
// kb-deploy infra status --json
{ "ok": true, "services": [
  { "name": "qdrant", "image": "qdrant/qdrant:latest", "running": true,  "state": "running" },
  { "name": "redis",  "image": "redis:7-alpine",       "running": false, "state": "stopped" }
]}
 
// error — structured diagnostic envelope (shared by all kb-* launcher tools)
// `error.code` is a stable ERR_-prefixed identifier; `reason` (why) and
// `hint` (what to do / where to look) are present when the tool supplies them.
{ "ok": false, "status": "error", "error": {
  "code": "ERR_CONFIG_LOAD",
  "message": "deploy config not found: searched up from cwd looking for .kb/deploy.yaml",
  "hint": "run kb-deploy from the project root, or pass --config <path>"
}}

CI usage

YAML
# .github/workflows/deploy.yml
- name: Deploy affected targets
  env:
    SSH_HOST: ${{ secrets.SSH_HOST }}
    SSH_USER: ${{ secrets.SSH_USER }}
    SSH_KEY:  ${{ secrets.SSH_KEY }}
  run: kb-deploy run --json

kb-deploy run exits 0 even when there is nothing to deploy (no affected targets). It exits 1 only when a target fails — CI will catch it.

Declarative apply (ADR-0014)

run is imperative: build this image, push it, restart that container. apply is declarative: you describe the desired fleet state in deploy.yaml, and kb-deploy computes and executes the plan to reach it.

The division of labor:

  • kb-deploy apply is the fleet orchestrator. It opens SSH to each host, collects current state, computes a rollout plan, delivers config, and drives the waves.
  • kb-create install-service is the per-host installer (one host = one installer). apply calls it on each target to install code and swap the active release atomically.
  • kb-dev owns service lifecycle and health on the host.

The mechanics below are covered by unit and e2e tests. End-to-end rollout on a live VM is being validated — treat apply as the declarative model and command surface, not yet as a copy-paste production tutorial.

deploy.yaml (declarative block)

YAML
schema: kb.deploy/1
 
platform:
  version: 2.94.0
  registry: https://registry.npmjs.org
  config: kb.config.jsonc        # pre-rendered runtime config, delivered verbatim
 
rollout:
  autoRollback: true
  parallel: 1                    # hosts per wave
  lockMode: artifact             # "artifact" (default) | "autoCommit"
 
hosts:
  vm-1:
    ssh:
      host: 203.0.113.10
      user: deploy
      key_path_env: KBD_SSH_KEY  # env var holding a path to the private key
    platformPath: ~/kb-platform
    env:                         # per-host secrets/endpoints → host .env
      MONGODB_URI: ${secrets.MONGODB_URI}
      REDIS_URL:   ${secrets.REDIS_URL}
      OPENAI_API_KEY: ${secrets.OPENAI_API_KEY}
 
services:
  gateway:
    service: "@kb-labs/gateway"
    version: 2.94.0
    targets:
      hosts: [vm-1]
      strategy: all              # "all" | "canary"
      healthGate: 30s
  # rest-api, workflow, state … same shape

The split is deliberate: deploy.yaml is WHERE/HOW (which hosts, which config file, what waves). The rendered kb.config.jsonc is WHAT (adapterOptions: mongo/redis/qdrant URIs, keys). deploy.yaml never carries adapterOptions — that keeps deploy and config authoring separate.

Config delivery

All daemons on a host share one installer-owned <platformPath>/.kb/kb.config.jsonc (the config loader is per-install). apply delivers it plus a resolved per-host .env:

  • Pre-rendered, verbatim. kb-deploy does not synthesize the config from deploy.yaml — you ship a file rendered by kb-create or by hand. ${VAR} references in it resolve against the host .env.
  • Atomic write. Per file: back up the current one to .prev, stream the new content to a temp file under umask 077, verify it is non-empty, then mv into place on the same filesystem. A dropped connection mid-write can never replace a working config with a truncated one.
  • Secret-safe. Resolved secret values travel over stdin, never argv — they don't leak into ps or shell history. They are never written to the lock or to disk on the control machine.
  • Config hash. apply records sha256(jsonc + resolved env) per host in the lock. Unchanged hash → no restart. Changed hash with the same release → a config-only restart (so rotating a secret is picked up even when no code changed).

Preflight

Everything that can fail without touching a host fails first, on the control machine, before any SSH is dialed:

  • the config file exists and parses as JSONC;
  • every ${secrets.X} / ${env.X} resolves (a missing or empty value is an error, not a silent blank credential);
  • no service targets a host that isn't defined.

A broken config or missing secret aborts with zero partial mutation.

Rollout and rollback

Services roll out in waves gated by healthGate. With autoRollback: true, a wave that fails health triggers a rollback: the affected host's config is restored from .prev before the previous release is restarted, so the old release never comes up against new config. Delivery itself runs as a pre-pass across all hosts — if it fails on any host, apply aborts before any install and restores already-written hosts.

Commands

Bash
kb-deploy plan                 # compute the rollout plan, touch nothing
kb-deploy apply --dry-run      # preflight + plan, no host mutation
kb-deploy apply                # execute (prompts unless --yes)
kb-deploy apply --yes          # execute without the confirmation prompt

Exit codes: 0 no changes / dry-run, 1 changes applied, 2 error (validation, SSH, build), 3 rollback fired and succeeded.

Today apply targets single-instance deployments (one replica per service). A config-only change restarts each daemon through the health gate, which is a brief downtime window on one instance. Multi-replica rolling restart is the k8s path — the ConfigDeliverer seam maps the same rendered config → ConfigMap and .env → Secret without changing the planner.

KB Labs integration

Inside KB Labs, kb-deploy is configured via .kb/deploy.yaml in the workspace root. Application targets (web, docs, API) are deployed by CI on every push to main. Infrastructure (Qdrant, Redis) is brought up once with infra up and left running — run never touches it.

Use kb-monitor to observe deployed services and infrastructure from the same config file.

kb-deploy — KB Labs Docs