Harbor Cookbook ยท copyable operator recipes

Ship without guessing.

A practical guide for installing Harbor, registering endpoints, adopting workspaces, deploying apps, checking runtime state, and recovering when something goes sideways.

Fast deploydeploy --unit app --endpoint meow
Local test deploydeploy force --unit app
Previewdeploy --unit app --dry-run
Find contextharbor, harbor unit list

Mental model

Harbor is a standalone CLI. The core path is local context plus explicit endpoint state.

EntityMeaningWhere to look
WorkspaceRepo or monorepo rootharbor.config.jsonc
UnitDeployable app/packageharbor unit list
ServiceRuntime process/static siteharbor service list
EndpointNamed SSH/runtime hostharbor endpoint
ReleaseOne deployed snapshotharbor deploy history
1. Discover workspace and unit.
2. Resolve endpoint, port, domain, service.
3. Preflight, upload, activate, record.

Install Harbor

Install the CLI launchers, including the short deploy binary.

Normal install

local user
bun run harbor:install
harbor self doctor

Replace launchers

force
bun run harbor:install:force
exec zsh

Completions

zsh/bash
harbor self install-completions
exec zsh
harbor complete -- deploy run --unit ""

Add an endpoint

An endpoint is the named SSH/runtime host Harbor deploys to.

With explicit SSH details

recommended
harbor endpoint add meow meow --user deploy --identity-file ~/.ssh/id_ed25519
harbor endpoint set-default meow
harbor endpoint sync-ssh-config
harbor endpoint doctor meow

Using an existing SSH alias

quick
harbor endpoint add meow meow
harbor endpoint set-default meow
ssh meow true
harbor endpoint doctor meow

Harbor-managed SSH config is kept between # >>> harbor endpoints >>> and # <<< harbor endpoints <<<.

Adopt a workspace

Use this when you are in a monorepo and want Harbor to discover deployable apps.

Discover, adopt, inspect

monorepo
harbor workspace inspect
harbor workspace adopt
harbor workspace adopt --write
harbor unit list
harbor unit inspect apm

Create a simple Bun app

A minimal Node-compatible Bun/TypeScript service that Harbor can build locally and run remotely with Node.

Scaffold

copy all
mkdir -p apps/my-app/src
cat > apps/my-app/package.json <<'JSON'
{
  "name": "my-app",
  "private": true,
  "type": "module",
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "bun build src/server.ts --target=node --outdir=dist",
    "start": "node dist/server.js"
  },
  "devDependencies": {
    "@types/node": "^24.0.0"
  }
}
JSON
cat > apps/my-app/src/server.ts <<'TS'
declare const process: { env: Record<string, string | undefined> };
export {};

const { createServer } = await import("node:http");
const host = process.env.HOST ?? "127.0.0.1";
const port = Number(process.env.PORT ?? "3000");
const html = "<!doctype html><h1>Hello from Harbor</h1>";

createServer((_req, res) => {
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
  res.end(html);
}).listen(port, host, () => {
  console.log(`listening on http://${host}:${port}`);
});
TS

Build locally

preflight
bun run --cwd apps/my-app typecheck
bun run --cwd apps/my-app build
node apps/my-app/dist/server.js

Add to Harbor

unit
harbor unit add my-app --path apps/my-app --preset app
harbor unit inspect my-app

Deploy

deploy is a shortcut for Harbor's deploy flow, not a second runtime.

Short path

normal
deploy --unit my-app --endpoint meow

Equivalent Harbor command

explicit
harbor deploy run --unit my-app --endpoint meow

Preview

safe
deploy --unit my-app --endpoint meow --dry-run

One-off domain

override
deploy to meow using my-app.example.de --unit my-app

Deploy local test code

Use this when changes are uncommitted or the app directory is untracked. It still runs preflight.

Force local working tree

testing
deploy force --unit my-app --endpoint meow

See what will run

dry-run
deploy force --unit my-app --endpoint meow --dry-run

--force means "package this local working tree deliberately". It does not skip typecheck, test, or build.

Hook-triggered local deploys

Manual deploy is the primary path. Hooks are explicit local automation, not a remote daemon.

Install and inspect

local git
harbor ci install --unit my-app --endpoint meow
harbor ci status
harbor ci logs

Manual trigger / skip

control
harbor ci trigger
HARBOR_HOOK_DEPLOY=0 git commit --allow-empty -m "skip harbor deploy"

Runtime, DNS, nginx, SSL

Use Harbor for the high-level view; SSH remains the direct escape hatch when debugging a host.

Domain and endpoint

checks
harbor domain inspect my-app.example.de
harbor endpoint doctor meow
curl -I https://my-app.example.de/

Service logs

debug
harbor runtime logs --service my-app
ssh meow 'sudo journalctl -u harbor-my-app --no-pager -n 80'

Nginx

remote
ssh meow 'sudo nginx -t'
ssh meow 'sudo systemctl reload nginx'

Secrets

provider
harbor secret set cloudflare/default
harbor secret list
harbor secret test cloudflare/default

History and rollback

Harbor records local receipts and release metadata so rollback can be audited.

History

local/remote
harbor deploy history
harbor deploy history --sync --write --unit my-app --endpoint meow

Rollback

safe first
harbor deploy rollback --dry-run --unit my-app --endpoint meow
harbor deploy rollback --unit my-app --endpoint meow
harbor deploy rollback <release-id> --unit my-app --endpoint meow

Troubleshooting

No config

setup
harbor workspace inspect
harbor workspace adopt --write
harbor unit list

Multiple units

selection
harbor unit list
deploy --unit <unit> --dry-run
deploy --unit <unit>

SSH broken

endpoint
harbor endpoint
harbor endpoint inspect meow
ssh meow true
harbor endpoint doctor meow

Source changed, page did not

dist
deploy force --unit <unit> --endpoint meow --dry-run
deploy force --unit <unit> --endpoint meow

Advanced config

Auto internal port

service
{
  "services": {
    "api": {
      "listen": {
        "type": "port",
        "host": "127.0.0.1",
        "port": "auto",
        "range": "31000-31999"
      },
      "env": {
        "PORT": "${harbor.port}",
        "HOST": "${harbor.host}"
      }
    }
  }
}

Static artifact

site
{
  "units": {
    "web": {
      "preset": "static",
      "path": "apps/web",
      "build": { "cwd": "workspace", "command": "bun run --filter web build" },
      "source": { "strategy": "artifact", "artifact": "apps/web/dist" },
      "services": ["web"]
    }
  }
}

State locations and safety

PathPurpose
harbor.config.jsoncworkspace units/services/stacks
.harbor/runs/local command receipts
.harbor/releases.jsonlocal release index
~/.config/loom/harbor/config.jsonendpoints and UI preferences
~/.local/share/loom/harbor/registry.jsonworkspace/deploy registry
/srv/harbor/apps/<name>/remote app release tree
/etc/systemd/system/harbor-<service>.serviceremote systemd service
status commands are read-only dry-run previews force still preflights secrets stay redacted