Skip to content

Deploy: Kubernetes and VM

Tobby ships in three forms, all from the same reproducible release chain: a Helm chart (with a plain-manifest twin), Linux packages (.deb, .rpm, .apk), and a container image. Whatever the form, one rule dominates the layout.

Tobby writes to two directories:

  • The store (storage.root) holds artifacts, recipes, and operation logs. Large, self-contained, and refetchable.
  • The state (state.root) holds the instance’s identity: accounts, tokens, served certificates, resumable-download spools. Small, and nothing recreates it.

The state directory is the backup target; the store costs only bandwidth to rebuild. The two must never be nested in one another — secrets do not travel on transportable media — and Tobby refuses to start when they are. The Helm chart additionally refuses to render when both mount paths are equal. See Operate over time.

The reference deployment comes two ways: a Helm chart, published with every release as an OCI artifact at oci://ghcr.io/tobby-fetch/charts/tobby, and the same pod written out as numbered plain YAML under deploy/manifests/, for clusters that do not run Helm. Both produce a non-root container on a read-only root filesystem with every capability dropped, probes wired, and separate PersistentVolumeClaims for store (100Gi default) and state (20Gi default — the state also spools resumable downloads, see Operate).

Terminal window
helm install tobby oci://ghcr.io/tobby-fetch/charts/tobby --version 0.6.0 \
--namespace tobby --create-namespace \
--set config.mode=passthrough

The chart version is the Tobby version: chart 0.6.0 deploys the v0.6.0 image, and nothing else. Pin --version — without it Helm takes the newest chart, which is an upgrade you did not choose. Each published chart is signed keylessly by the release workflow, like the image:

Terminal window
cosign verify ghcr.io/tobby-fetch/charts/tobby:0.6.0 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/tobby-fetch/tobby-fetch/'

A cluster with no route to ghcr.io gets the chart the way it gets everything else: the chart and the image it points at are two ingredients of a recipe, carried in by Tobby and pulled from the zone registry (oci://registry.zone.example/…). From a checkout of the repository, ./deploy/charts/tobby is the same chart, for development.

config.mode is the one value with no usable default: an instance must state what it is (FR-001). The chart’s config value becomes /etc/tobby/config.yaml and mirrors Tobby’s own configuration keys exactly — the output of tobby config dump pastes in as-is. env sets TOBBY_* variables and extraArgs appends flags; precedence is flags > environment > file (FR-003). Four keys (storage.root, state.root, server.addr, registries.credentialsFile) are owned by the chart and rejected if set directly, because the pod spec depends on them.

Registry credentials are a separate kubernetes.io/dockerconfigjson Secret (FR-004), mounted read-only — never in the configuration file, never in the store:

Terminal window
kubectl -n tobby create secret docker-registry tobby-registry-credentials \
--docker-server=registry.example.com \
--docker-username=tobby \
--docker-password="$(cat ./token)"
helm upgrade tobby oci://ghcr.io/tobby-fetch/charts/tobby --version 0.6.0 \
--namespace tobby --reuse-values \
--set registryCredentials.enabled=true \
--set registryCredentials.existingSecret=tobby-registry-credentials

Entries are looked up by the host actually contacted — with source substitution in play, the substitute registry, not the nominal one.

The files in deploy/manifests/ are numbered in apply order and create their own tobby namespace, which enforces the restricted Pod Security Standard — a later edit weakening the pod’s security context is rejected by the API server, not silently accepted.

Terminal window
kubectl apply -f ./deploy/manifests/

Edit 20-secret-config.yaml (set mode, and retriever.source if you have one) and 21-secret-registry-credentials.yaml (the shipped payload is an empty, valid {"auths":{}}) before applying.

The published image carries SLSA Build L3 provenance and a cosign signature, both made against the digest. Pin the digest in production (image.digest in the chart) and verify first:

Terminal window
cosign verify ghcr.io/tobby-fetch/tobby-fetch@sha256:... \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/tobby-fetch/tobby-fetch/'

The rolling latest tag is signed with the same identity but carries no SLSA provenance — only immutable vX.Y.Z digests do. The full verification guide, provenance included, is Verify a release.

Each release publishes .deb, .rpm, and .apk packages wrapping the exact tobby-linux-<arch> release binary — installed to /usr/bin/tobby, no install scripts, fully self-contained (CGO disabled). They are built for repository-less, offline installation: verify (SHA256SUMS or SLSA provenance), copy the file across, install with no network:

Terminal window
dpkg -i tobby_0.6.0_linux_amd64.deb # Debian/Ubuntu
rpm -i tobby_0.6.0_linux_amd64.rpm # RHEL/SUSE/Fedora
apk add --allow-untrusted tobby_0.6.0_linux_amd64.apk # Alpine

The packages deliberately carry no package-manager signature: trust comes from the provenance or checksum verification performed before installation — --allow-untrusted on apk states exactly that. The reasoning is spelled out in Verify a release.

The packages install the binary only; the service definition is yours. This reference unit mirrors the container deployment’s posture:

/etc/systemd/system/tobby.service
[Unit]
Description=Tobby instance
After=network-online.target
Wants=network-online.target
[Service]
User=tobby
Group=tobby
ExecStart=/usr/bin/tobby serve --config /etc/tobby/config.yaml
Restart=on-failure
# SIGTERM starts the graceful drain; give it more than
# shutdown.gracePeriod (default 30s) before SIGKILL.
TimeoutStopSec=60
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/tobby/store /var/lib/tobby/state
[Install]
WantedBy=multi-user.target

Create the two directories owned by the service user, put storage.root and state.root in /etc/tobby/config.yaml, and keep secrets out of the unit file — the proxy password, for instance, belongs in the configuration file or in an environment file readable only by the service user (see Enterprise network).

ghcr.io/tobby-fetch/tobby-fetch is a minimal, shell-less image running as UID 65532. Mount the two volumes and the configuration:

Terminal window
docker run -d --name tobby --read-only \
-p 8080:8080 \
-v /srv/tobby/store:/var/lib/tobby/store \
-v /srv/tobby/state:/var/lib/tobby/state \
-v /srv/tobby/config.yaml:/etc/tobby/config.yaml:ro \
ghcr.io/tobby-fetch/tobby-fetch:v0.6.0 \
serve --config /etc/tobby/config.yaml
Platform Tier
Linux amd64 / arm64 Validated production scope: passthrough service, packages, container image.
Windows amd64 / arm64 Single static binary published, and the mirror workstation journey is validated on it (NFR-018). Passthrough on Windows is outside the validated scope. See supported platforms.
macOS amd64 / arm64 Convenience tier (NFR-001): same reproducible chain, distributed via brew install tobby-fetch/tap/tobby, full test suite in CI — but no production support implied.

The store refuses filesystems that cannot hold its layout — FAT32 among them; see limits.

Authentication is on by default, and Tobby refuses to serve until the state directory holds at least one account (TBY-AUTH-001) — a fresh Kubernetes install sits in CrashLoopBackOff until the administrator exists, which is intended: no surface is ever exposed open. On a terminal, tobby quickstart walks you through it. For automation, every question has a flag, and the password arrives on standard input — never on the command line:

Terminal window
printf '%s\n' "$ADMIN_PASSWORD" | tobby quickstart \
--mode passthrough \
--config /etc/tobby/config.yaml \
--storage-root /var/lib/tobby/store \
--state-root /var/lib/tobby/state \
--password-stdin

The equivalent primitive commands are tobby user add admin --state-root /var/lib/tobby/state --password-stdin followed by tobby serve --config /etc/tobby/config.yaml. The first account of an instance is always admin, and the tool computes the password hash itself. On Kubernetes, where the image has no shell, run the user add step in a short-lived pod mounting the state claim — the exact recipe is in deploy/README.md.

If you must run open, auth.disabled: true is an explicit opt-in that puts a permanent warning banner in the UI (FR-075). Never leave it on.

Signing in through the corporate directory

Section titled “Signing in through the corporate directory”

OIDC and SAML are off by default, and the local admin stays — it is the break-glass account when the directory is down. To enable either, three things to get right at install time (details on Enterprise identity):

  • Callback URLs live under /auth/. Register exactly https://<host>/auth/oidc/callback (OIDC) or the assertion consumer service https://<host>/auth/saml/acs (SAML; the SP descriptor is https://<host>/auth/saml/metadata). Configuration naming any other path is refused at startup. A reverse proxy in front must pass /auth/ through.
  • Pin the SAML identity provider. Prefer auth.saml.idpMetadataFile; auth.saml.idpMetadataURL requires the signing certificate pinned in auth.saml.idpCertificateFile.
  • Mount the secrets outside the store: auth.oidc.clientSecretFile and auth.saml.keyFile, like every secret path.

displayName labels the sign-in button; auth.oidc.nameClaim and auth.saml.nameAttribute choose the name shown for a person. All three are display only. Upgrading from v0.5.x needs none of this to keep running: every new federation key is off or defaulted.

One default does change when you upgrade from v0.5.x: vulnerability scanning now runs, in advisory mode. Every image retrieved is scanned — whatever kind it is declared as — there is no “enabled” switch — and with scan.mode left at warn the findings are reported (task report, API, interface, metrics, with the database age) while nothing is withheld. An instance with no scanner or no database shows the degraded-mode banner instead. To refuse what violates a threshold, set scan.mode: block (or TOBBY_SCAN_MODE=block; the threshold is scan.threshold, 7.0 by default), or pose a threshold for one perimeter: a Retriever’s spec.scanPolicy.threshold or a recipe’s recipe.tobby.dev/scan-threshold annotation, each blocking for what it covers. A refusal withholds the whole recipe. See vulnerability scanning.

Next: point the instance at the outside world through the enterprise network.