.. _howto-runners: Pair your own runners ===================== Goal: your builds run on machines you control. saggar.dev queues the jobs and serves the artifacts; a runner is the machine that does the building. A home PC, a workstation or a fleet all work. A runner needs no inbound port and holds no store credentials: it polls for jobs and streams everything through the service. 1. Prepare the machine ---------------------- A runner needs: - unprivileged user namespaces with subuid/subgid entries for the service user (image-environment builds), - podman, the builder for docker-format trees, - binfmt/qemu for foreign-arch targets. There is no capability matrix to register. A machine missing what a job needs fails the job, naming the missing tool. Pre-flight the machine before pairing: .. code-block:: console $ saggar-worker doctor It probes user namespaces by actually creating one, checks ``newuidmap``/``newgidmap`` and the subuid/subgid entries, reports the builders (podman required when the configured builder is podman; docker informational) and the data dir (writable, free space). When the machine's ``[network]`` configures a ``registry_mirror``, the mirror is probed too: one short-timeout HTTP GET, informational. A mirror down at doctor time is a warning about the mirror, not a failing machine. Non-zero exit when a required piece is missing. 2. Pair the runner ------------------ Pairing names an account. The runner joins as the account that owns the presenting token. On a machine where ``saggar login`` has run, the stored token pairs it: .. code-block:: console $ saggar-worker pair The ``pair`` subcommand pairs and exits; the next plain start claims jobs. Anywhere else, pass an account token: .. code-block:: console $ saggar-worker --account-token sgt-… The pairing is admitted inline; the token proves the account, so approving is pairing. The runner appears on your account's Fleet page in the web app. The issued runner token persists under the data dir (``0600``) and is reused from there on; the account token is only consulted on first start. A dead account token (401/403) stops the worker with a clear error; transport errors retry. On approval the worker prints a ready-to-paste systemd user unit (its ``ExecStart`` re-runs the flags the worker was started with; no credential ever lands in it, the unit leans on the persisted token) plus the ``systemctl --user enable --now`` and ``loginctl enable-linger`` steps, so a paired runner becomes a service in one paste. Infra-as-code instead: ``--token`` (env ``SAGGAR_WORKER_TOKEN``) presents a pre-provisioned runner token and skips pairing entirely. The service address defaults to ``https://api.saggar.dev``; for another instance, pass ``--hub``. The runner also takes ``--config`` and ``--data-dir``, and runs one build at a time. Declare the conduct profile --------------------------- Pass the machine's posture on the same command line. The service records what the owner declares, never probes, and shows it to anyone considering sharing with the runner: ``--network online|pinned|offline`` The schedulable one. ``offline`` declares no outbound network: the service only offers the runner jobs whose source is an uploaded tarball (the service streams the tree itself); a git-source job waits for a network-capable runner. ``pinned`` declares egress through mirrors: the pull-side pins live in the runner's ``[network]`` config, where ``registry_mirror`` routes docker-format base-image pulls through a pull-through cache (see :ref:`reference-config`). Scheduling treats ``pinned`` like ``online``: the declaration is about *how* the machine reaches the network, not whether it can. ``--no-docker-format`` Declares the machine cannot build docker-format trees at all (no podman builder installed); the service stops offering it docker-format jobs. Default: it can. A re-pair re-declares; a body that omits the fields pairs with the safe defaults. Host execution stays off. A build that cannot resolve to a container environment fails at bootstrap, naming the gap. 3. Watch the fleet ------------------ .. code-block:: console $ saggar node ls The roster shows the runners you own and the runners shared with you: owner and ``owned`` flag on every row, next to the declared profile. Share a runner (as its owner) through the API or the web app: .. code-block:: console $ curl -X PUT -H "X-Auth-Token: sgt-…" -d '{"handle": "ada"}' \ https://saggar.dev/api/v1/nodes/build-01/shares Sharing grants use: a shared account's jobs may schedule on the runner, eligibility re-evaluated per claim. Revoking a share (``DELETE …/shares/ada``) stops future claims instantly while a running job finishes its lease. Retire a runner you own with ``saggar node retire build-01`` (its token dies immediately; the row stays listed ``retired``). A job no eligible runner can take (wrong arch, a git source with only offline runners, docker format with no capable runner) stays queued, visible in the jobs list. Configuration ------------- The runner reads a ``saggar.toml``, and only the build-side sections mean anything on it: ``data_dir``, ``[envs]``, ``[incremental]``, ``[network]``, ``[atlas]`` and the ``[plugins]`` knobs. They are documented in :ref:`reference-config`. A ``[storage]`` section on a runner is ignored with a loud warning: every artifact byte streams through the service under the job's lease (see :doc:`../reference/worker-api`), and store credentials do not belong on a build machine.