This commit is contained in:
2026-09-03 11:28:30 +07:00 Unverified
parent 26c2d0f015
commit 82d0fe3e0d
93 changed files with 19237 additions and 1285 deletions
+387 -85
View File
@@ -1,45 +1,280 @@
# kuber
`kuber` is Astral's internal Docker Compose to Kubernetes translation layer.
`kuber` is a Docker Compose to Kubernetes translation layer backed by a
self-hosted management service (`kuber-server`). It reads a local Compose file,
renders Kubernetes resources, and applies them to the cluster through an
authenticated v2 API. Image builds happen inside the cluster with rootless
BuildKit, so the workstation needs neither Docker nor `kubectl`.
It reads a local Compose file, renders Kubernetes resources, applies them to the cluster, and can build images remotely from your working tree without requiring Docker on your machine.
## Architecture
## What It Does
```
workstation (kuber CLI) ──HTTPS──▶ kuber-server (in-cluster pod)
│
├─ CAS (content-addressed blobs on RWX PVC)
├─ BuildKit Jobs (rootless) ──▶ registry
└─ Kubernetes API (RBAC-scoped)
```
- Reads `compose.yml` or `docker-compose.yml`
- Converts supported Compose services into Kubernetes resources
- Applies those resources into a namespace named after the current directory
- Turns `env_file` entries into Kubernetes Secrets and mounts them through `envFrom`
- Translates file mounts into ConfigMaps and directory/volume mounts into PVC-backed volumes
- Builds images on the remote builder by syncing:
- committed git state
- tracked local diffs
- untracked files
- ignored `.env*` files
- Supports host-based `ports` syntax that renders Kubernetes `Ingress` rules
- Supports managed Postgres claims through special pseudo-volumes such as `postgresql:app`
- Supports managed S3 buckets and credentials through pseudo-volumes such as `s3:app`
The CLI authenticates to the server with a Bearer token acquired by `kuber
login`. It snapshots the repository into a content-addressed workspace, uploads
only the blobs the server is missing, and submits a build request. The server
materializes that workspace onto a shared RWX PVC and runs a rootless BuildKit
Job that builds and pushes the image, then the CLI pins the resulting immutable
registry digest into the rendered Deployment.
The server also owns reconciliation: it plans, applies, and prunes resources in
a per-project namespace, reconciles managed Postgres and S3 claims, rolls
deployments back, streams logs, and exposes interactive `exec` sessions over a
WebSocket.
## Environment Assumptions
This tool is built for Astral's cluster and workstation setup. It is not intended to work unchanged outside that environment.
kuber targets a specific self-hosted cluster and workstation setup. It is not
intended to run unchanged against an arbitrary Kubernetes environment. The
expected setup:
Expected local setup:
- Tailscale access to the remote builder and Kubernetes network
- a working `~/.kube/config`
- `ssh` available locally
- an account on the `kuber-server` management API
- a working ClusterRole/Role (see [Server Deployment](#server-deployment))
- a registry the in-cluster BuildKit can push to
Not required locally:
- Docker
- `kubectl`
Docker is not required because builds happen on the remote builder after `kuber` syncs your repo state there. `kubectl` is not required because cluster access is handled through the bundled Kubernetes client.
## API Origin
The v2 management API has one hard-coded origin:
```text
https://kuber.astrxl.dev/api/v2
```
## Authentication
```bash
kuber login dmgnr
kuber login dmgnr --persist
kuber whoami
kuber logout
```
The default login is stored with mode `0600` under
`$XDG_RUNTIME_DIR/kuber/session.json` and disappears with the user runtime
directory. `--persist` instead uses `$XDG_CONFIG_HOME/kuber/session.json`, or
`~/.config/kuber/session.json` when `XDG_CONFIG_HOME` is unset. Password input
is never echoed. Runtime sessions last 24 hours and persistent sessions last 30
days; `logout` revokes the server-side session. `readSession` falls back to the
persistent file when no runtime session exists.
The default login is scoped to the current user and the authenticated identity
is available to every v2 command. `login`, `logout`, and `whoami` are the only
commands that run without a loaded project configuration.
### Roles and Authorization
The server grants capabilities through three roles:
- `viewer` — read-only cluster access (`kubernetes:read`)
- `operator` — `viewer` plus `kubernetes:write` and `kubernetes:exec`
- `admin` — all capabilities, including user administration
(`users:read`, `users:write`, `sessions:revoke`)
Administer users with the `users` command tree:
```bash
kuber users ls
kuber users add dmgnr --roles admin
kuber users update dmgnr --roles operator
kuber users update dmgnr --password
kuber users disable dmgnr
kuber users enable dmgnr
kuber users delete dmgnr
kuber users revoke dmgnr
```
`users add`, `update --password`, and `delete` prompt for password / written
confirmation on an interactive terminal. Passwords are hashed with Argon2id on
the server and never stored in plaintext. Updating a user's roles or password
revokes all of that user's active sessions.
Inspect server-side operations and the audit trail:
```bash
kuber operations ls
kuber operations get <operation-id>
kuber audit ls
```
## Server Deployment
The repository's `compose.yml` owns the `kuber-system` namespace, the
`kuber-server` image, its Deployment, Service, and Ingress. `.kuberrc.ts`
extends the rendered manifests with the server's ServiceAccount and RBAC:
- a namespaced `Role`/`RoleBinding` (`kuber-server-auth`) for the `kuber-system`
Secrets, ConfigMaps, Pods, Jobs, and Leases the server itself reads and writes
- a `ClusterRole`/`ClusterRoleBinding` (`kuber-server-manager`) granting the
cross-namespace verbs it needs to manage user projects
`compose.yml` runs the server as a non-root user (`runAsUser`/`runAsGroup`
`1000`), drops all Linux capabilities, uses a read-only root filesystem with a
`RuntimeDefault` seccomp profile, and backs `/data` with a Longhorn PVC
(`kuber-build-data`). The PVC hosts the CAS, materialized workspaces, and
resumable upload bytes, and is shared with BuildKit Jobs.
Deploy the server with kuber itself:
```bash
KUBER_BOOTSTRAP_PASSWORD='replace-me' kuber up
```
The server creates an `admin` user (from `KUBER_BOOTSTRAP_USERNAME`, default
`dmgnr`) on first boot only if that user does not already exist. The bootstrap
Secret is only rendered while `KUBER_BOOTSTRAP_PASSWORD` is set. After logging
in successfully, reconcile without that variable and restart once so kuber
removes the stale bootstrap Secret and the password leaves the pod environment:
```bash
kuber up --no-b
kuber restart kuber-server
```
See [Account Recovery](#account-recovery) for what to do if you are locked out.
### Security Constraints
Because the server's ServiceAccount is scoped by the RBAC in `.kuberrc.ts`, it
can only act on the resources kuber manages. The management layer additionally
enforces ownership, so the server refuses to mutate resources that do not carry
kuber's workspace labels. Deleting a resource requires its UID as an optimistic
concurrency precondition, and workspace deletion also requires the namespace
UID. Namespaces that are not owned by kuber, or owned by a different workspace,
are never mutated (see [Workspaces and Migration](#workspaces-and-migration)).
## Workspaces and Migration
Each project maps to a Kubernetes namespace derived from the current working
directory. The CLI records a "workspace" on the server keyed by project name and
borrows the namespace UID to guarantee it owns the namespace before reconciling.
Adopting existing resources relabels them (Server-Side Apply) only when they are
already managed by kuber and not owned by another workspace.
`up` refuses to mutate a namespace when:
- the namespace already exists but does not carry kuber's managed-by label
(`external`), or
- the namespace is labeled for a different workspace UID
(`different-workspace`)
In those cases the CLI prints a hint to `POST
/workspaces/<project>/adopt` with the namespace UID to complete a safe,
explicit adoption. The platform namespace (`kuber-system`) is adopted through
the admin-only platform adoption route.
Workspace state, revisions, operations, and audit events are stored as Secrets
and ConfigMaps in `kuber-system` keyed by kuber's `kuber.astrxl.dev/type`
label. Workspace updates are optimistic (If-Match on resource version) and
immutable revisions are recorded so history survives. Expired sessions are
cleaned up on an interval, and stale operations are marked failed on server
startup recovery.
### Migration Behavior
`kuber up` is idempotent and safe to re-run. Each run:
1. snapshots the workspace and uploads missing blobs to the server CAS,
2. ensures the workspace record (creating or updating it with an optimistic
If-Match),
3. adopts the namespace and its kuber-managed resources,
4. reconciles managed Postgres and S3 claims,
5. renders manifests, plans the diff, applies desired resources, waits for
rollout, and deletes stale resources.
Because resource references are immutable digests, re-running `up` only restarts
deployments whose image content actually changed. `start` re-resolves published
digests without building.
## Registry Authentication
Building and pushing images from inside the cluster typically requires
credentials for the target registry. These are read from a Docker config file
(`KUBER_REGISTRY_CONFIG`, default `/etc/kuber/registry/config.json`) and mounted
as an image pull secret named by `KUBER_REGISTRY_SECRET`.
Registry authentication is optional. If `KUBER_REGISTRY_SECRET` is unset, the
server warns at startup and BuildKit uses anonymous registry access. This is
intended for registries that allow anonymous push/pull. The same credentials are
used when the server resolves a published image digest (`start` / `export`).
The server supports both standard registry bearer-token (`WWW-Authenticate:
Bearer`) and pre-emptive Basic auth when resolving digests.
### Build Registry Environment
The server's registry behavior is driven by a few closely related environment
variables:
- `KUBER_BUILD_REGISTRY` (default `registry.neko-piranha.ts.net`): the registry
the in-cluster BuildKit pushes built images to and `kuber` uses as the image
namespace. It also supplies the registry host for authentication.
- `KUBER_INTERNAL_REGISTRY_HOST`: the host of an internal registry (for example
the in-cluster distribution service) used for the BuildKit cache image and,
when set, the image direct push target. When unset, push and cache fall back
to `KUBER_BUILD_REGISTRY`.
- `KUBER_INTERNAL_REGISTRY_INSECURE`: set to `"true"` to push to the internal
registry over plain HTTP instead of HTTPS. Only meaningful when
`KUBER_INTERNAL_REGISTRY_HOST` is set.
- `KUBER_PUSH_IMAGE_PREFIX` (default `kuber/`): a prefix applied to project
images pushed to the internal registry when `KUBER_INTERNAL_REGISTRY_HOST` is
configured.
- `KUBER_REGISTRY_RESOLVE_ORIGIN`: an explicit origin used to resolve a
published image digest (used by `start` / `export`). Useful when the digest
must be resolved from a different endpoint than the build/push registry, such
as an internal HTTP registry.
- `KUBER_REGISTRY_CONFIG`: path to the Docker config file with registry
credentials (see above).
- `KUBER_REGISTRY_SECRET`: the image pull Secret mounted for BuildKit's
registry access.
- `KUBER_BUILDKIT_IMAGE`: override the BuildKit runner image used for builds.
- `KUBER_BUILD_DATA_CLAIM`: the PVC claim backing builds.
See [Server Deployment](#server-deployment) for how `compose.yml` wires the
internal registry variables for the kuber-server pod.
## Account Recovery
If you lose your credentials and cannot log in:
1. Recreate the bootstrap admin by deploying with
`KUBER_BOOTSTRAP_PASSWORD` set again:
```bash
KUBER_BOOTSTRAP_PASSWORD='new-password' kuber up --no-b
kuber restart kuber-server
```
2. The server only creates the bootstrap user if the account does not already
exist, so a fresh `kuber login <username>` with the new password works, or
use the newly created admin to reset other accounts:
```bash
kuber users update <username> --password
```
3. After recovering, reconcile without `KUBER_BOOTSTRAP_PASSWORD` and restart
so the bootstrap Secret is removed and the password leaves the pod
environment.
Because user records and session hashes are stored as Secrets in
`kuber-system`, recovery relies on cluster administrators being able to redeploy
the server with bootstrap credentials. `users revoke <username>` forcibly logs a
user out across all devices.
## Next.js Example
[`example/`](example/) contains a documented deployment template for adding kuber to an existing Bun-powered Next.js project without initializing or bundling an application in this repository. It includes a standalone-output Dockerfile, `.dockerignore`, `compose.yml`, and the required Next.js configuration.
[`example/`](example/) contains a documented deployment template for adding
kuber to an existing Bun-powered Next.js project without initializing or
bundling an application in this repository. It includes a standalone-output
Dockerfile, `.dockerignore`, `compose.yml`, and the required Next.js
configuration.
## Running
@@ -72,16 +307,23 @@ bun run index.ts db ls
bun run index.ts s3 ls
bun run index.ts s3 creds app
bun run index.ts s3 ui app
bun run index.ts login dmgnr
bun run index.ts users ls
bun run index.ts operations ls
bun run index.ts audit ls
```
All commands accept `--config` to use a configuration file other than
`.kuberrc.ts`:
```bash
kuber --config deploy/production.kuberrc.ts up
kuber up --config deploy/production.kuberrc.ts
kuber --config production.kuberrc.ts up
kuber up --config production.kuberrc.ts
```
`login`, `logout`, and `whoami` are context-free and do not require a
configuration.
### Shell Completion
Generate and load completions for your shell:
@@ -91,27 +333,43 @@ source <(kuber complete zsh)
source <(kuber complete bash)
```
For a permanent setup, write the generated script to a file and source it from your shell configuration. Fish and PowerShell are also supported through `kuber complete fish` and `kuber complete powershell`.
For a permanent setup, write the generated script to a file and source it from
your shell configuration. Fish and PowerShell are also supported through
`kuber complete fish` and `kuber complete powershell`.
## Commands
- `up`: build images if needed, pin matching registry digests, render manifests, apply them, and wait for rollout
- `start`: same as `up` but resolves the currently published image digests instead of building
- `up [--no-b]`: build images if needed, ensure the workspace, reconcile managed
Postgres/S3, render manifests, apply them, wait for rollout, and delete stale
resources
- `start`: like `up` but re-resolves the currently published image digests
instead of building
- `login [username] [--persist]`: authenticate with the kuber API
- `logout`: revoke and remove the current API session
- `whoami`: show the authenticated API user and roles
- `users`: administer user accounts and roles
- `operations`: inspect server-side reconciliation operations
- `audit`: inspect the audit trail
- `ps [-a]`: print an ANSI graph of the current project namespace, hiding stopped
deployments by default
- `logs [deployment] [-f]`: print (or follow) logs for one deployment or all
managed deployments
- `exec <deployment> <command...>`: execute a command inside a running
deployment pod over an interactive WebSocket
- `restart [deployment]`: roll out a restart across managed deployments
- `stop`: scale managed deployments to zero
- `restart`: roll out a restart across managed deployments
- `rollback` (alias `fuck`): roll one deployment back to its previous release, or all managed deployments when no name is given
- `down`: delete managed resources while keeping ingress, PVCs, managed databases, and managed S3 storage
- `down -f`: also delete ingress, PVCs, managed database and S3 resources, and the namespace
- `ps`: print an ANSI tree of the current project namespace, hiding stopped deployments by default
- `ps -a`: include stopped deployments and stale ReplicaSets in the tree
- `logs [deployment]`: print logs for one deployment or all managed deployments
- `logs -f [deployment]`: follow logs continuously
- `exec <deployment> <command...>`: execute a command inside a running deployment pod
- `db ls`: list managed Postgres claims declared in the current Compose file
- `db creds <service>`: reconcile and print the generated connection details for a managed Postgres claim
- `s3 ls`: list managed S3 claims declared in the current Compose file
- `s3 creds <service>`: print all generated S3 environment variables for a service
- `s3 ui <service>`: print the Garage UI object-browser URL for a service bucket
- `rollback` (alias `fuck`) `[deployment]`: roll one deployment back to its
previous release, or all managed deployments when no name is given
- `down [-f]`: delete managed resources while keeping ingress, PVCs, managed
databases, and managed S3 storage; `-f` also deletes those and the namespace
- `db ls` / `db creds <service>`: list or print credentials for managed Postgres
claims
- `s3 ls` / `s3 creds <service>` / `s3 ui <service>`: list managed S3 claims,
print their credentials, or print the Garage UI URL for a bucket
- `export [-o file]`: render manifests to a YAML file without applying them
Lifecycle commands (`restart`, `stop`, `rollback`, `down`) are idempotent:
identical requests are deduplicated server-side and tracked as operations.
## Configuration
@@ -125,11 +383,6 @@ export default {
project: "my-app",
composeFile: "compose.production.yml",
registry: "registry.example.com",
builders: {
amd64: "kuber@amd-builder",
arm64: "kuber@arm-builder",
remoteRoot: "kuber-build",
},
rolloutTimeoutMs: 10 * 60_000,
async compose(compose) {
@@ -147,37 +400,50 @@ export default {
Operational defaults:
- `project`: Compose top-level `name`, falling back to the current working directory name
- `project`: Compose top-level `name`, falling back to the current working
directory name
- `composeFile`: the first recognized Compose filename in the working directory
- `registry`: `registry.neko-piranha.ts.net`
- `builders.amd64`: `kuber@astral-th`
- `builders.arm64`: `kuber@astral`
- `builders.remoteRoot`: `kuber-build`
- `rolloutTimeoutMs`: `300000`
Project-name precedence is `.kuberrc.ts project`, Compose top-level `name`, then
the current working directory name.
The `registry` value controls which registry the CLI requests build images from
and which the server uses to resolve published digests. `rolloutTimeoutMs`
bounds how long `up` and `rollback` wait for a Deployment rollout.
Configuration hooks can be synchronous or asynchronous and receive mutable
values:
- `compose(compose, context)`: once after parsing and validation; affects every command that reads Compose
- `preBuild(compose, context)`: before build eligibility is evaluated when builds are enabled
- `postBuild(result, context)`: after images are built; `result` contains `built` and `changed` service names
- `postRender(resources, context)`: after rendering and before reconciliation planning; also runs for `export`
- `postApply(resources, context)`: after desired resources are successfully applied
- `compose(compose, context)`: once after parsing and validation; affects every
command that reads Compose
- `preBuild(compose, context)`: before build eligibility is evaluated when
builds are enabled
- `postBuild(result, context)`: after images are built; `result` contains
`built` and `changed` service names
- `postRender(resources, context)`: after rendering and before reconciliation
planning; also runs for `export`
- `postApply(resources, context)`: after desired resources are successfully
applied
Hook context contains the resolved `cwd`, `project`, `composeFile`, and optional
`configFile`. A hook error aborts the command and is reported by the normal CLI
error handler.
Registry and rollout configuration remain part of the CLI-facing contract.
Image builder selection is not configurable: builds are scheduled, executed, and
owned entirely by the server.
## Compose Conventions
`kuber` supports a few project-specific Compose conventions on top of normal service translation.
`kuber` supports a few project-specific Compose conventions on top of normal
service translation.
### Host-Based Ports
If a `ports` entry uses a hostname instead of a numeric published port, `kuber` treats it as an ingress host and routes traffic to the target container port.
If a `ports` entry uses a hostname instead of a numeric published port, `kuber`
treats it as an ingress host and routes traffic to the target container port.
Example:
@@ -188,9 +454,11 @@ services:
- somedomain.astrxl.dev:3000
```
That produces a Kubernetes `Ingress` rule for `somedomain.astrxl.dev` pointing at the service port for container port `3000`.
That produces a Kubernetes `Ingress` rule for `somedomain.astrxl.dev` pointing
at the service port for container port `3000`.
Single-level wildcard subdomains are supported. Quote wildcard entries so YAML does not treat the leading `*` as an alias:
Single-level wildcard subdomains are supported. Quote wildcard entries so YAML
does not treat the leading `*` as an alias:
```yml
services:
@@ -200,7 +468,8 @@ services:
- "*.secure.astrxl.dev:3001:protected"
```
Protected routes use the kuber dialect and render Traefik `IngressRoute` resources instead of plain Kubernetes `Ingress`:
Protected routes use the kuber dialect and render Traefik `IngressRoute`
resources instead of plain Kubernetes `Ingress`:
```yml
services:
@@ -254,7 +523,8 @@ services:
- s3:app
```
This creates `GarageBucket/app` and `GarageKey/app` in `garage-system`. To use different key and bucket names:
This creates `GarageBucket/app` and `GarageKey/app` in `garage-system`. To use
different key and bucket names:
```yml
services:
@@ -263,7 +533,8 @@ services:
- s3:app-key/shared-assets
```
The Garage operator generates the credentials. `kuber` reads its generated Secret and injects these values into the service's `<service>-env` Secret:
The Garage operator generates the credentials. `kuber` reads its generated
Secret and injects these values into the service's `<service>-env` Secret:
- `AWS_ACCESS_KEY_ID`
- `AWS_SECRET_ACCESS_KEY`
@@ -271,7 +542,9 @@ The Garage operator generates the credentials. `kuber` reads its generated Secre
- `AWS_REGION`
- `S3_BUCKET`
One service can declare both `postgresql:...` and `s3:...`; all generated values are merged into the same service Secret. Managed Garage buckets and keys are retained by normal `down` and deleted by `down -f`.
One service can declare both `postgresql:...` and `s3:...`; all generated
values are merged into the same service Secret. Managed Garage buckets and keys
are retained by normal `down` and deleted by `down -f`.
Inspect a claim, print its generated credentials, or get its Garage UI URL:
@@ -287,9 +560,11 @@ assignments. `s3 ui` only prints the URL; it does not open a browser.
### Environment Files
`env_file` entries are read locally and turned into a Kubernetes `Secret` named `<service>-env`. Deployments then consume that secret through `envFrom`.
`env_file` entries are read locally and turned into a Kubernetes `Secret` named
`<service>-env`. Deployments then consume that secret through `envFrom`.
This is also where generated values such as `DATABASE_URL` and the managed S3 environment are injected.
This is also where generated values such as `DATABASE_URL` and the managed S3
environment are injected.
### Volumes
@@ -382,12 +657,27 @@ shared across projects and intentionally retained when a project is removed.
## Building
Image builds run through the selected SSH builder. After a push, the registry's
manifest digest is captured and embedded into the rendered Deployment as an
immutable `:latest@sha256:...` reference, so a changed image naturally triggers
a rollout (there is no separate "restart changed deployments" step). `start` and
`export` look up the currently published digest without rebuilding, and fail if
a buildable service has no published image yet.
When a service declares `build`, the CLI:
1. snapshots the repository into a content-addressed workspace
(committed git state, tracked changes, untracked files, and ignored `.env*`
files),
2. negotiates with the server and uploads only the blobs it is missing,
3. submits a build request; the server materializes the workspace from the CAS
onto a shared RWX PVC and runs a rootless BuildKit Job that builds and pushes
the configured image with registry cache.
Build containers are restricted: they run as non-root (`runAsUser`/`runAsGroup`
`1000`), do not mount the service account token, and only read the workspace
(read-only mount) and a writable BuildKit state `emptyDir`. After a push, the
registry's manifest digest is captured and embedded into the rendered Deployment
as an immutable `:latest@sha256:...` reference, so a changed image naturally
triggers a rollout. `start` and `export` look up the currently published digest
without rebuilding, and fail if a buildable service has no published image yet.
`export` is side-effect-free: it refuses to render managed Postgres or S3 claims
(because it cannot call the server to generate credentials) and instead tells
you to run `kuber up` or remove the managed provider claims.
## Rollback
@@ -407,23 +697,35 @@ Notes and limitations:
- A later `kuber up` or `kuber start` re-resolves `:latest` and returns the
Deployment to the current desired state anyway, so rollback is the right tool
for responding to a bad deploy, not for permanently pinning an old version.
- Rollback only considers Deployments managed by kuber (`app.kubernetes.io/managed-by=kuber`).
- Rollback only considers Deployments managed by kuber
(`app.kubernetes.io/managed-by=kuber`) and only runs within a workspace whose
namespace kuber owns.
To build distributable binaries:
## Workspace State and Operations
The server persists per-workspace state, revisions, operations, and audit
events as Secrets/ConfigMaps in `kuber-system`. Mutations are idempotent:
- operations carry an idempotency key so retries do not double-apply,
- resource deletion requires a UID precondition,
- workspace replacement is guarded by If-Match.
The CLI is built with the normal `build` script for distribution and local
use:
```bash
bun run build.ts
```
If you only want a plain JavaScript bundle for quick local use in another workspace, build `index.ts` directly:
```bash
bun build index.ts --target bun --minify --sourcemap --outdir dist
bun run build
```
## Notes
- Resource names and namespaces are derived from the current working directory.
- The remote build flow is optimized for local iteration, not for producing a perfectly clean export of the repository.
- Managed database support is Kubernetes-only. It injects `DATABASE_URL` into the generated app secret and does not rewrite local `.env` files.
- `kuber` operates on managed resources in the namespace matching the current directory name.
- The workspace snapshot is optimized for local iteration, not for producing a
perfectly clean export of the repository.
- Managed database support is Kubernetes-only. It injects `DATABASE_URL` into
the generated app secret and does not rewrite local `.env` files.
- `kuber` operates on managed resources in the namespace matching the current
directory name.
- Builds are scheduled, executed, and owned by the server. There is no local
SSH/daemon builder configuration; `registry` and `rolloutTimeoutMs` remain
CLI-facing settings.