HWS: Routing & Deployments

heyctl

heyctl is a kubectl-style command-line client, and a Rust client library, for the app-lb admin API: deployments, VM pools, secrets, tokens, auth providers, builds, pulls, artifact stores, and the telemetry the obs plugin collects. The crate is published on crates.io as hws, the Heyo Web Services SDK; the binary it installs is heyctl.

What it is

app-lb is the HWS load balancer and autoscaler. Everything it does is driven through its admin API (default http://127.0.0.1:9090), and heyctl is the CLI for that API. Its verbs follow kubectl's: you apply declarative specs, use imperative helpers (create, scale, set) that write those specs for you, and read them back with get, describe and top.

It is a separate crate from app-lb (app-lb/heyctl), so installing it does not pull in pingora, openssl or the ACME stack. It shares only the wire format with the server. The same crate is also a library: build it with default-features = false to get the typed client without clap or a terminal.

A deployment is one of three kinds, and many commands only apply to one of them:

managed (vm) static (upstreams) site (site)
Backends an autoscaled pool of microVMs fixed host:port addresses none: files served from disk by app-lb
scale, restart, delete vm yes rejected rejected
cordon / drain / uncordon rejected yes rejected
exec / shell yes rejected rejected
set image / set env yes rejected rejected
set build / build, set artifact / pull yes (one or the other, not both) rejected rejected
set update / update rejected yes yes
set upstreams rejected yes rejected
set auth yes yes yes

When the server rejects a command for a deployment kind, heyctl passes the server's reason through.

Install

Installer

curl -fsSL https://heyo.computer/heyctl/install.sh | sh

The script is app-lb/heyctl/install.sh. It reads a version manifest (<site>/heyctl/versions.json), downloads the matching blob anonymously from the artifact store, verifies it against its sha256 digest and against the SHA256SUMS inside the tarball, and installs heyctl into ~/.local/bin.

Pass flags through the pipe with sh -s --. Without the -s --, sh reads the flags as its own:

curl -fsSL https://heyo.computer/heyctl/install.sh | sh -s -- --prefix /usr/local
curl -fsSL https://heyo.computer/heyctl/install.sh | sh -s -- --list
curl -fsSL https://heyo.computer/heyctl/install.sh | HEYCTL_VERSION=0.2.0 sh
Flag Meaning
--prefix PATH Install into PATH/bin (default ~/.local)
--version VER Install this version (default: the manifest's latest)
--digest SHA256 Install this exact blob and skip the manifest
--list Show the versions the manifest offers; install nothing
Env var Default Meaning
HEYCTL_BASE_URL https://heyo.computer Site serving heyctl/versions.json
HEYCTL_MANIFEST_URL unset Full manifest URL; overrides HEYCTL_BASE_URL
HEYCTL_STORE_URL the manifest's store Artifact store base URL
HEYCTL_VERSION the manifest's latest Version to install
HEYCTL_DIGEST unset Install this blob directly (rollback, or a link you were given)
HEYCTL_PREFIX $HOME/.local Install prefix
HEYCTL_NO_VERIFY unset Non-empty skips the SHA256SUMS cross-check. The blob digest is always verified

The installer never sends an Authorization header. The artifact store serves public blobs only to anonymous requests, so an ART_API_KEY in your environment is ignored on purpose. Redirects may not downgrade from HTTPS to HTTP.

It detects linux/darwin and x86_64/aarch64, and tells you which platforms the manifest actually offers if yours is missing.

From source

heyctl is the hws package in the app-lb Cargo workspace:

cargo install hws                    # from crates.io
# or, from this repository:
cd app-lb
cargo build --release -p hws
install -m 0755 target/release/heyctl ~/.local/bin/

As a library (the Heyo Web Services SDK):

[dependencies]
hws = { version = "0.2", default-features = false }

The library is async. The blocking feature adds hws::blocking::Client, which is what the CLI uses. The crate README has a quick start that creates a workload, reads its telemetry and rolls it out with a namespace token; examples/namespace_workload.rs is the same program. cargo doc -p hws --no-default-features --open builds the API docs; the changelog lists what changed between releases.

Connecting

With no config file and no flags, heyctl talks to http://127.0.0.1:9090, app-lb's default admin listener. A local app-lb needs no setup.

The admin listener is plaintext HTTP on loopback by default. To reach a remote one, either tunnel it:

ssh -L 9090:127.0.0.1:9090 lb-host
heyctl --server 127.0.0.1:9090 get deployments

or front the admin listener with an app-lb TLS deployment (see examples/app-lb-admin.json) and log in to its HTTPS name. --insecure-skip-tls-verify accepts a self-signed certificate on an endpoint you control.

Do not point a context at a hostname behind a Google sign-in gate. heyctl cannot complete an OAuth flow, so every command fails with a 401 whatever credentials you store. Tunnel to the admin listener instead, or see Putting the dashboard behind Google.

Credentials, contexts and the config file

The config file

Contexts (server plus credentials) and artifact-store registries live in one JSON file:

Location When
--config PATH / HEYCTL_CONFIG if set
$XDG_CONFIG_HOME/heyctl/config.json (usually ~/.config/heyctl/config.json) default on Linux
the platform config dir (dirs::config_dir()) + heyctl/config.json default elsewhere
~/.heyctl/config.json fallback when there is no config directory

The file is written mode 0600 and its directory 0700. It holds passwords, tokens and API keys in plaintext. Don't print it, paste it, or commit it. heyctl config view redacts secrets unless you pass --show-secrets; heyctl config path prints the location. An empty file is treated as no file.

To keep credentials out of the file, store a command instead of the value (--password-command, --token-command, --api-key-command), or verify without storing (--no-store-password, --no-store-key) and supply the value through the environment.

How app-lb authenticates heyctl

app-lb accepts three credentials on its admin API. See app-lb auth for the server side.

Credential How heyctl sends it Reach
HTTP Basic (APP_LB_DASHBOARD_USER / APP_LB_DASHBOARD_PASSWORD) --user / --password the whole fleet
App-token applb_… --token whatever it was minted with
Heyo API key heyo_api_… or Heyo JWT --token the namespaces the Heyo auth service grants

A namespace-scoped Heyo API key is used against Cloud's namespace door, https://<cloud>/namespaces/<ns>/lb, and reaches only that namespace at the tier it was minted with (view or admin).

Heyo account login and regional visibility

For a platform administrator account, use --email, not --user (gateway Basic auth):

heyctl login --server https://admin.heyo.work --email sam@heyo.computer
heyctl get deployments --fleet
heyctl get deployments --fleet --namespace default -o json

Login prompts for the account password and uses the same HTTPS /login exchange as the dashboard. It saves only the expiring session token in the existing permission-restricted config. Run login again when it expires; no password is retained for unattended refresh. Redirects and insecure TLS are not allowed.

--fleet reads the gateway's configured fleet, showing each deployment's gateway, region, health and unavailable-region errors. JSON/YAML preserves the server's full response, including truncation and error fields. It does not discover servers from DNS, change write targets, or fail over the selected admin endpoint. Without --fleet, existing reads and all deployment writes remain regional. Use an explicit regional context for host maintenance; fleet observation does not turn apply or restart into a coordinated release.

Precedence

For each of the password, token and artifact API key, heyctl uses the first of:

  1. the flag or env var (--password/HEYCTL_PASSWORD, --token/HEYCTL_TOKEN, --api-key/HEYCTL_ART_API_KEY)
  2. the context's stored *_command, run through sh -c on each request
  3. the context's stored value

A token outranks a user/password on the same context. The username defaults to admin; login never prompts for it, so pass --user if the server sets APP_LB_DASHBOARD_USER to something else. A wrong username gives the same 401 as a wrong password.

Global options

These work on every command, before or after the subcommand.

Flag Env var Default Meaning
-o, --output FORMAT table table, wide, json, yaml, name
--config PATH HEYCTL_CONFIG see above Config file
--context NAME HEYCTL_CONTEXT current context Which stored context to use
--server URL HEYCTL_SERVER context, else http://127.0.0.1:9090 Admin API URL. host:port means http
--user NAME HEYCTL_USER context, else admin Basic-auth user
--password PASSWORD HEYCTL_PASSWORD Basic-auth password. Visible in ps; prefer the env var
--token TOKEN HEYCTL_TOKEN Bearer token (applb_… or heyo_api_…). Visible in ps; prefer the env var
--insecure-skip-tls-verify off Accept any TLS certificate
--request-timeout SECS 30 Per-request timeout

-o json and -o yaml print the server's payload unmodified, so they round-trip into apply. -o name prints deployment/<id> lines for xargs.

Scripts and CI can skip the config file entirely:

HEYCTL_SERVER=https://cloud.example.com/namespaces/team-a/lb \
HEYCTL_TOKEN="$HEYO_KEY" heyctl get deployments

login, logout, whoami

heyctl login --server 127.0.0.1:9090                       # prompts for a password if the server wants one
heyctl login --server https://lb-admin.example.com --user admin
heyctl login --server lb.example.com:9090 --password-command 'pass show app-lb/admin'
heyctl login --server lb.example.com:9090 --no-store-password   # then export HEYCTL_PASSWORD
heyctl login --server https://cloud.example.com/namespaces/team-a/lb --token-stdin <<< "$HEYO_KEY"
heyctl whoami
heyctl logout --keep-context                               # drop the stored password only

login probes the server, verifies the credentials against whatever is actually gated, and saves a context (named after the server's host unless you pass --name). A token login verifies with GET /deployments, since Cloud's namespace door has no /healthz to probe.

login flag Meaning
--server URL Admin API to log in to
--user NAME Basic-auth user (default admin)
--password, --password-stdin, --password-command CMD Password source; the command form stores the command, not the password
--token, --token-stdin, --token-command CMD Log in with a bearer token instead
--no-store-password Verify, but don't write the password or token to disk
--name NAME Context name
--no-switch Save without making it current
--insecure-skip-tls-verify Accept any certificate

whoami prints the config file, context, server, user, where the password comes from, whether the server is reachable, which routes require auth, and whether the deployment API and /metrics are allowed for this identity. It is the first thing to run when you get a 401. It also reports the current artifact registry.

config

Command Does
config view [--show-secrets] Print the config file, secrets redacted
config get-contexts List contexts
config current-context Print the current context
config use-context NAME Switch contexts
config set-context NAME [--server] [--user] [--password] [--password-command] [--insecure-skip-tls-verify] [--current] Create or update a context
config delete-context NAME Remove a context
config path Print the config file path

--context NAME on any command uses a context once without switching.

Command reference

Resource names take kubectl's forms: deployments, deployment web, deployment/web, deploy web, or a bare web where the kind is unambiguous. Kinds and aliases:

Kind Aliases
deployment deploy, dep, d, app
vm instance, backend, replica
cert certificate
secret sec
workflow wf, flow, ci
job build, pull, update, run
disk pv, volume, vol, storage
namespace ns
auth-provider provider, idp
all

Reading

Command Main flags Does
get RESOURCE... (alias list) -d/--deployment, -n/--namespace, -w/--watch, --interval SECS (2) List any kind above
describe RESOURCE -n/--namespace (auth providers) Spec, pool, backends, traffic, gate, mounts; or an auth provider
top [deployments|vms|host] -w/--watch, --interval CPU, memory, latency and 5xx ranking
status (alias cluster-info) Uptime, host, fleet and traffic totals
feed [NAMESPACE] --xml, -n/--limit N A namespace's event feed; with no argument, the namespaces that have events
heyctl get deployments -o wide          # adds MIN MAX WARM TARGET BACKEND SOURCE AUTH
heyctl get deployment/web -o yaml
heyctl get vms -d web
heyctl get secrets -n team-a            # ids and key names, never values
heyctl get jobs -d web                  # builds, pulls, updates and mount pulls, newest first
heyctl get job job-3f2a1c8e             # one job, with its log
heyctl get disks -d sb-7f3a9c
heyctl get auth-providers -n team-a
heyctl describe deployment web
heyctl top vms -w

Creating and applying

Command Main flags Does
create deployment NAME (deploy, dep) see below Register a deployment
create secret NAME [KEY=VALUE]... (sec) -n, --from-file KEY=PATH, --from-env KEY[=VAR], --from-stdin KEY, --description, --dry-run Store write-only secret values
create namespace NAME (ns) --description, --dry-run Declare a namespace (fleet admin only)
create auth-provider NAME (provider, idp) -n, --preset, --issuer, --client-id, key and claim flags Declare a namespace auth provider; see app-lb auth
create workflow ID (wf, flow) --repo (required), --network (required), --ref (main), --path (.ci/workflows/*.yml), --secrets-prefix, --disabled Register a CI workflow
apply -f FILE -f/--filename (repeatable, - for stdin), --dry-run Create or replace from JSON or YAML: one spec, a JSON array, or a multi-document YAML stream. Objects with kind: auth-provider are upserted as providers
edit RESOURCE Open the server's JSON in $VISUAL/$EDITOR and PUT it back. A rejected edit is kept on disk

create deployment flags, by group:

Group Flags
General -n/--namespace (default default), --dry-run
Routing --host, --host-suffix, --path-prefix (together one rule), --route RULE (repeatable: host=a.example.com,path=/api, *.example.com, /api), --no-route
VM pool --image, --port (required for managed), --driver (firecracker; libvirt is rejected), --start-command, --size (micro..xlarge), --disk-gb, --workdir, -e/--env KEY=VALUE, --setup-hook, --open-port, --ttl
Static site --site-root DIR, --site-index, --site-404, --site-spa, --site-cache-control
Static upstreams --upstream ADDR (repeatable), --discovery-service SERVICE
Build source --repo or --build-store, --ref, --dockerfile, --build-context, --image-name, --size-mb, --secret NAME[/KEY]
Scaling --min, --max, --warm, --target-concurrency, --scale-to-zero-after, --cold-start-timeout, --drain-timeout, --boot-timeout, --idle-action destroy|retain
Health --health-path (default /), --health-tcp, --health-port, --health-timeout

--path-prefix is forwarded unchanged; app-lb does not strip it. --disk-gb is the only guest storage that survives a VM stop, because the root filesystem is recopied from the image on every boot.

Secret values passed as KEY=VALUE arguments are visible in ps and shell history. Prefer --from-file, --from-env or --from-stdin.

Editing in place

Every set command is a read-modify-write of the whole spec (PUT /deployments/:id). heyctl edits the server's JSON rather than its own struct, so fields it does not know about survive. All set commands take --dry-run.

Command Main flags Does
set image RESOURCE IMAGE Change a managed deployment's image; recycles the pool
set env RESOURCE KEY=VALUE... / KEY- Set or remove guest env vars; recycles the pool
set upstreams RESOURCE ADDR... Replace a static deployment's upstream list
set route RESOURCE (routes) --host, --host-suffix, --path-prefix, --route, --add, --none Replace or extend route rules; --none withdraws a managed deployment from the proxy
set build RESOURCE --repo or --store, --ref, --dockerfile, --build-context, --image-name, --size-mb, --secret NAME[/KEY] (default key token), --username, --no-auth, --clear Record where build gets its Dockerfile
set artifact RESOURCE (art) --store URL|PATH, --ref, --image-name, --grow-gb, --secret, --no-auth, --clear Record where pull gets a rootfs
set update RESOURCE --workdir (absolute, on the app-lb host), -c/--command (repeatable), -e/--env, --secret-env [ENV=]NAME/KEY, --secret, --no-auth, --command-timeout, --verify-timeout, --clear Record how update redeploys a static deployment or site
set auth RESOURCE --provider-ref or --client-id/--secret/--allow-domain/--allow-email; --public-path, --base-path, --session-ttl, --cookie-name, --no-forward-identity, --clear Put a deployment behind a sign-in gate; see app-lb auth
set secret RESOURCE [KEY=VALUE|KEY-]... (sec) -n, --from-file, --from-env, --from-stdin, --description Rotate keys; unmentioned keys keep their values

Passing any --command, --env, --secret-env, --allow-domain, --allow-email or --public-path replaces that whole list. Use heyctl edit for incremental changes.

set auth --public-path writes a bare-string entry, which app-lb reads as scope admin: the path skips Google sign-in but requires an admin-tier app-token. To make a path fully open (a health check, a webhook), write {"path": "/healthz", "scope": "public"} with heyctl edit or apply. See public paths.

set build and set artifact are mutually exclusive on one deployment, because both rewrite vm.image.

Jobs: build, pull, update, mounts

These start an asynchronous job on the app-lb host. Without --wait they return the job id straight away; follow it with heyctl get job <id>. One job runs per deployment at a time; a second is refused, not queued. A failed job makes heyctl exit non-zero after printing the tail of the log.

Command Main flags Does
build RESOURCE --ref (one-off), -w/--wait, --logs (implies --wait), --timeout (1800) Build the image with heyvm mvm build and roll the pool
pull RESOURCE --ref (one-off), --force, -w/--wait, --logs, --timeout (1800) Pull a rootfs from an artifact store and roll the pool
mounts pull RESOURCE --force, -w/--wait, --logs, --timeout (1800) Unpack vm.mounts tarballs; the pool rolls only if a digest changed
update RESOURCE -w/--wait, --logs, --timeout (1800) Run a static deployment's update commands, then re-probe its upstreams

pull --ref <digest> pins exact bytes without changing the stored spec, which is how you roll back. A tag follows wherever it is moved. apply and edit start a mount pull automatically when a mount has no tree on the host.

An update whose commands succeed but whose upstreams never come back is reported as a failure. The commands run as app-lb's user.

Scaling and lifecycle

Command Main flags Does
scale RESOURCE (autoscale) -r/--replicas N, --min, --max, --warm, --target-concurrency, --scale-to-zero-after, --cold-start-timeout, --drain-timeout, --boot-timeout, --idle-action Partial PATCH of the scaling policy; unset fields keep their values
restart RESOURCE --force, --wait, --timeout (300) Drain every VM; the autoscaler boots replacements
rollout status RESOURCE --timeout (300), --no-wait Wait until desired equals ready and nothing is draining
rollout restart RESOURCE as restart Same as restart
cordon RESOURCE UPSTREAM --force, --reason Stop new requests to one static upstream
drain RESOURCE UPSTREAM --force, --reason, --timeout (300) Cordon, then wait for in-flight requests to finish
uncordon RESOURCE UPSTREAM Return a cordoned upstream to traffic once healthy
delete RESOURCE... (rm) -d/--deployment, -n/--namespace, --all, --force, -y/--yes Delete deployments, VMs, secrets, workflows, namespaces or auth providers

--replicas N pins min = max = N. Give --min/--max again to hand control back to the autoscaler.

--idle-action retain stops an idle VM instead of destroying it, and a later request or exec resumes it. Only the /workspace data disk (--disk-gb) persists across that stop.

Cordon state is durable across app-lb restarts. A drain is refused when no other healthy upstream would remain, unless you pass --force. On timeout the upstream stays cordoned.

delete vm recycles a VM: the autoscaler replaces it if the policy still wants the capacity. Use scale to shrink. delete secret is refused while a deployment references the secret; --force deletes it anyway. Certificates, jobs and disks cannot be deleted with delete.

Getting inside a VM

Command Main flags Does
exec RESOURCE -- CMD... --cwd, -e/--env, --timeout (60), --no-wake Run one command through sh -c in the guest. stdout, stderr and the exit code pass through
shell RESOURCE (ssh) --cwd, --no-wake, --vm SANDBOX_ID Interactive PTY

Both go through app-lb, not the heyvm daemon, so they work wherever the admin API does. Both start a VM if the deployment has none running (up to its cold_start_timeout_secs), unless you pass --no-wake. An open shell counts as in-flight work, so the pool will not scale to zero under it. These are the only way into a deployment created with --no-route.

Tokens

App-tokens are app-lb's own scoped, revocable bearer credentials. See app-lb auth for the scope model.

Command Main flags Does
token mint NAME --admin none|view|admin (default none), -d/--deployment ID (repeatable), --all-deployments, --namespace NS, --expires-in HOURS, -q/--quiet Mint a token. The secret is printed once
token list List live tokens; never shows secrets
token describe ID Show one token
token set ID --name, --admin, -d/--deployment, --all-deployments, --never-expires Re-scope without changing the secret
token revoke ID -y/--yes Revoke; takes effect on the next request

mint writes the secret to stdout and everything else to stderr, so capturing it is safe:

APP_LB_TOKEN=$(heyctl token mint ci --admin admin --all-deployments -q)

A token scoped to specific deployments cannot mint tokens, so it cannot widen itself.

Plugins

Command Does
plugins list (alias plugin) List plugins and whether each is enabled
plugins describe ID Configuration and live status
plugins enable ID / plugins disable ID Toggle; configuration is kept
plugins set ID -f FILE [--enable] Replace a plugin's configuration from JSON (- for stdin)
plugins list -n NS Plugins a namespace can install, and whether it has
plugins install ID [-n NS] [-f FILE] Install a per-namespace plugin into a namespace (needs admin there)
plugins uninstall ID [-n NS] Uninstall it
plugins installs ID Every namespace a plugin is installed in (fleet scope)

enable/disable are the operator's fleet-wide switch. Per-namespace plugins (obs) also have to be installed in a namespace before they do anything there. Where -n is optional, it defaults to the namespace the token is confined to.

Telemetry

Command Does
logs DEPLOYMENT [-n NS] A deployment's logs, oldest first: --since 1h, --level error, --grep TEXT, --backend SANDBOX, --limit N
top -n NS [--window 1h] [-w] Every deployment in a namespace with request/error rates, latency, CPU, memory and log counts over the window

Both read app-obs through app-lb's obs plugin, so they need it installed in the namespace (heyctl plugins install obs -n NS) and nothing beyond a namespace token. top without -n still shows the LB's live counters.

Artifact stores

An artifact store (art serve) is a separate service from app-lb, so heyctl artifact (aliases art, registry) keeps its own saved registries in the same config file. --context never retargets an artifact command.

Registry option Env var Meaning
--registry NAME HEYCTL_REGISTRY Which saved registry
--registry-url URL HEYCTL_ART_URL Store URL override (host:port means http)
--api-key KEY HEYCTL_ART_API_KEY Store API key override
Command Main flags Does
artifact login URL --api-key, --api-key-stdin, --api-key-command, --no-store-key, --name, --no-switch, --insecure-skip-tls-verify Verify a store key and save a registry
artifact logout [NAME] --key-only Forget a registry, or just its key
artifact registries (contexts) List saved registries
artifact use NAME (use-registry) Switch registry
artifact push [FILE] --image NAME, --tag, --no-tag, --force Upload an ext4 rootfs and tag it
artifact push-dockerfile FILE (push-df) --build-context PATH, --tag, --no-tag, --image-name, --size-mb, --source, --force Upload a Dockerfile and context as a heyvm.dockerfile.v1 manifest
artifact ls (tags) List tags
artifact describe REF What a tag or digest resolves to
artifact usage Logical size, physical size, free space
artifact untag NAME (rm-tag) Remove a tag; the blob stays until art gc

push --image NAME resolves ~/.heyo/images/firecracker/<name>.ext4 (or under $MVM_DATA_DIR), where heyvm mvm build writes images. The tag defaults to the file name without .ext4. push-dockerfile's tag defaults to the Dockerfile's directory name. The build context is packed as-is, with no .dockerignore handling, so point it at a clean directory.

Shell completion

heyctl completion bash > /etc/bash_completion.d/heyctl
heyctl completion zsh  > ~/.zfunc/_heyctl

fish, elvish and powershell are also supported.

Common workflows

Deploy a spec

# web.yaml
id: web
routes: [{ host: web.example.com }]
vm:
  driver: firecracker
  image: nginx-fc
  port: 80
  size_class: mini
scaling: { min_replicas: 1, max_replicas: 4 }
health: { path: /healthz }
heyctl apply -f web.yaml --dry-run   # print what would be sent
heyctl apply -f web.yaml
heyctl rollout status web
heyctl describe deployment web

For field names, generate a spec with heyctl create deployment ... --dry-run or read an existing one with heyctl get deployment web -o yaml. Copying between load balancers is a pipe:

heyctl get deployment web -o json | heyctl --context staging apply -f -

The imperative equivalent:

heyctl create deployment web --host web.example.com --image nginx-fc --port 80 \
  --size mini --min 1 --max 4 --health-path /healthz

Build from a Dockerfile

heyctl create secret github --from-stdin token < ~/.github-pat
heyctl set build web --repo https://github.com/acme/web.git --ref main --secret github
heyctl build web --logs

Each build produces an image named <deployment>-<short sha>, so describe and get -o wide show which commit is running.

Push and pull an image

Build the image once, push it to a store, and let each app-lb host pull it:

heyctl artifact login https://art.us2.heyo.work --api-key-stdin < ~/.art-key
heyctl artifact push --image web-v2

heyctl create secret art --from-stdin api_key < ~/.art-key
heyctl set artifact web --store https://art.us2.heyo.work --ref web-v2 --secret art/api_key
heyctl pull web --wait
heyctl get jobs -d web

A store root on the app-lb host (--store /srv/artifacts) is much cheaper than a URL: the image is materialised locally and sparse regions are skipped. Re-pulling unchanged bytes skips the transfer but still rolls the pool.

Follow logs

Application logs come from the obs plugin, once it is installed in the namespace:

heyctl plugins install obs -n team-a
heyctl logs web -n team-a --since 15m --level error
heyctl top -n team-a

Job output comes with the job:

heyctl build web --logs        # stream build output, then wait
heyctl pull web --logs
heyctl update app-obs --logs
heyctl get job job-3f2a1c8e    # status plus log tail of any job

Without the plugin, run a command in the guest (heyctl exec web -- tail -n 100 /var/log/heyvm-start.log). Guest start-command errors live in that file, not in app-obs.

Scale

heyctl scale web --min 1 --max 8 --warm 2 --target-concurrency 20
heyctl scale web --replicas 3                  # pin
heyctl scale web --scale-to-zero-after 600
heyctl restart web --wait                      # rolling recycle
heyctl top

Agent sandbox

heyctl create deployment sb-7f3a9c --no-route --port 8080 --size medium --disk-gb 8
heyctl scale sb-7f3a9c --idle-action retain
heyctl exec sb-7f3a9c --cwd /workspace -- git status
heyctl shell sb-7f3a9c
heyctl set route sb-7f3a9c --host sb-7f3a9c.example.com   # expose it later
heyctl set route sb-7f3a9c --none                         # withdraw it again

Static upstream maintenance

heyctl drain stage us1.internal:8080 --reason 'kernel upgrade' --timeout 300
# ... maintenance ...
heyctl uncordon stage us1.internal:8080
heyctl get vms -d stage     # shows Draining separately from health

Work in a namespace

heyctl create namespace team-a --description "Acme"            # fleet admin
heyctl create deployment api -n team-a --host api.example.com --port 8080
heyctl create secret db -n team-a --from-env url=DATABASE_URL
heyctl token mint team-a-ci --admin admin --namespace team-a -q
heyctl get deployments -n team-a
heyctl plugins install obs -n team-a                             # telemetry for every app in it

A token minted with --namespace and no --deployment reaches every deployment in that namespace and nothing outside it. Secrets and auth providers are resolved per namespace: get secrets -n team-a is the only way to name one when ids collide across namespaces.

Exit codes

Code Meaning
0 Success (for exec, the guest command's exit code is passed through instead)
1 The command failed; the reason is on stderr as error: …
2 Usage error from the argument parser

Troubleshooting

Symptom Cause and fix
connection refused to 127.0.0.1:9090 No app-lb on this machine, or no tunnel. Run ssh -L 9090:127.0.0.1:9090 host, or heyctl login --server the right endpoint
Every command 401s, whoami shows deployment API and metrics denied Wrong credential, or the server is behind a Google gate that heyctl cannot pass. Tunnel to the admin listener instead
401 with the right password Wrong username. login defaults to admin; pass --user to match APP_LB_DASHBOARD_USER
get works but writes 401 The server has APP_LB_ADMIN_AUTH=1 and you have no credential, or your token is view tier
403 insufficient_scope Your token is valid but its deployment or namespace scope does not cover the target. A higher admin tier will not help; re-scope with token set
A scoped token cannot create deployment or token mint Fleet-wide routes are refused to deployment-scoped tokens by design
heyo_api_… key refused against app-lb directly A namespace key belongs at Cloud's https://<cloud>/namespaces/<ns>/lb door, or app-lb needs APP_LB_AUTH_URL set to resolve it
Artifact push 401 while everything else works Artifact commands use the registry key, not the context. Check heyctl whoami and heyctl artifact registries
build/pull refused with a conflict A job is already running for that deployment. Wait for it (get jobs -d NAME)
set artifact refused The deployment has a build source. Clear it with set build NAME --clear first (and vice versa)
scale, exec or restart refused The command does not apply to this deployment kind; see the table at the top
Deployment shows 0 ready after apply A mount has not been pulled, or boot is failing. Check heyctl describe and heyctl get jobs -d NAME
A --public-path still asks for a token Bare public paths mean scope admin. Write {"path": ..., "scope": "public"} with heyctl edit
exec hangs, then times out The guest command outlived --timeout (60s default), or the VM is cold-starting; raise --timeout

For the full spec format see app-lb and the app-lb README. The heyctl source README, app-lb/heyctl/README.md, has longer notes on each command.