> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blobhub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Provisioning

> Get a validated credential onto a CI runner, a container, a cluster, or a host — with no terminal involved

Every recipe below answers the same question for a different piece of infrastructure: how does a real,
validated API key reach `blobhub` or `blobhub-worker` when nobody is sitting at a terminal to type it in.
[Authentication](/cli/auth) and [Authentication and Profiles](/worker/auth) document the mechanism — a
literal flag, its own `-stdin` sibling, or a named `-env <VAR>` sibling, resolved before either binary even
considers prompting. This page is worked examples: the same sources, wired into the infrastructure that
actually runs BlobHub commands.

Two things apply to everything below, so they're stated once instead of five times:

* **Provision a [service account](/general/service-accounts)'s key, not a person's.** Every recipe here hands
  a credential to infrastructure nobody is watching in real time. A service account key means the Blobs,
  revisions and thread items that infrastructure produces are authored by, say, `ci-deploy` — not by whoever
  happened to set the pipeline up — and revoking it later never touches a person's own access.
* **Lead with `--api-key-stdin` (or `--api-key-env`), never the literal `--api-key`.** The literal flag sits in
  `ps` output for as long as the process runs and in shell history for as long as that file exists — exactly
  the two places a provisioning script tends to run. Every example below pipes or points at the key instead.
  The same three-source shape covers `--api-url` too, for anything not pointed at the default
  `https://api.blobhub.io/v1` — it's left out of the examples below to keep them focused on the key.

## When not to use `login` at all

An environment key resolves on its own, with no credentials file, no profile, and nothing to clean up:

```bash theme={null}
export BLOBHUB_API_KEY="$BLOBHUB_KEY"
blobhub workflow deploy -f manifest.yaml
```

That's the whole story for a process that starts, does one thing, and exits — see
[Configuration](/cli/configuration#a-key-from-the-environment-needs-no-file-at-all) for the CLI's side of it.
`login` earns its place over that one line when you need a **stored profile**, for three reasons that a bare
environment variable can't give you:

* **A name.** `--profile ci` is something a teammate can read in a service definition; a key sitting in one
  process's environment isn't addressable at all once that process is gone.
* **A key validated before anything depends on it.** `login` calls `GET /v1/users/me` and fails loudly,
  before any deploy or any turn runs, if the key is wrong. An unvalidated environment variable fails at the
  first real command instead — later, and with a less specific error.
* **For the worker specifically, a state tree that survives rotation.** A [state tree](/worker/auth#which-tree-a-run-uses)
  is named after where its key came from: a stored profile's tree is named after the **profile**, so
  rotating the key under `--profile prod` keeps the same tree — same cursors, same lock. A key supplied
  directly by `--api-key`/`BLOBHUB_API_KEY` gets a tree named after a digest **of that key**, so rotating it
  is, from the worker's point of view, moving to a brand new tree with no memory of the old one. Nothing
  breaks, but every cursor starts over.

That third point is why every recipe below that provisions a **worker** uses `login`, and why the CI recipe —
the one genuinely short-lived case — is the one place a bare environment variable is usually the right answer
for either binary.

## CI

A CI job's secret store hands the runner an environment variable, already masked in logs and gone the moment
the job's container or VM is torn down. For a `blobhub` command that runs once and exits, that's the entire
setup, on any CI platform:

```bash theme={null}
# GitHub Actions, GitLab CI, CircleCI, Buildkite -- the mechanism is the same everywhere:
# the platform's secret store becomes a masked environment variable inside the job.
export BLOBHUB_API_KEY="$BLOBHUB_KEY"
blobhub workflow deploy -f manifest.yaml
```

If the same job needs a **stored, validated** profile — because it's about to hand the key to something
longer-lived, like the four recipes below — script `login` against the variable the platform already
injected, rather than re-typing the key anywhere:

```bash theme={null}
blobhub login --profile ci --no-input --api-key-env BLOBHUB_KEY
```

This is also usually where the key in the next four recipes originates: a CI job's own secret store, pushed
into a Kubernetes `Secret`, a Docker secret, or an Ansible vault as one step of the same pipeline.

## One shape, three paths

Docker secrets, a Kubernetes mounted `Secret`, and systemd's `LoadCredential=` all exist for the same reason:
hand a process a **file** only it can read, instead of an environment variable that shows up in `docker
inspect`, `/proc/<pid>/environ`, or a child process's own environment. Whichever platform delivers it, the
command on the receiving end is identical — pipe the file into `--api-key-stdin`:

```bash theme={null}
blobhub-worker login --profile prod --no-input --api-key-stdin < /path/to/the/file
```

The next three sections are that one line with three different paths, and the platform-specific reason each
one chose a file over a variable in the first place.

<Warning>
  A mount that isn't actually there fails at the **shell**, not inside `login` — `< /path: No such file or
    directory` — before the binary ever runs. A mount that **is** there but empty is what produces `login`'s own
  [`INPUT_REQUIRED`](/worker/reference), since an empty pipe is treated as no source at all. The two look
  similar in a log and mean opposite things: one is a missing mount, the other is a secret that resolved to
  nothing.
</Warning>

## Docker and Compose secrets

A `docker run -e` or a Compose `environment:` entry is visible to `docker inspect` and to anything that can
`docker exec` into the container — indefinitely, for as long as the container runs. Docker secrets exist
specifically to avoid that: they're mounted as files under `/run/secrets/<name>`, readable only inside the
container that declared them, never inspectable from outside it.

Creating the secret (Swarm) or declaring it (Compose) both land the key at the same path:

```bash theme={null}
# Swarm: create the secret once, out of band
printf '%s' "$BLOBHUB_KEY" | docker secret create blobhub_api_key -
```

```yaml theme={null}
# Compose: a file-backed secret, mounted at /run/secrets/blobhub_api_key by default
services:
  blobhub-worker:
    image: yourrepo/blobhub-worker:latest
    secrets:
      - blobhub_api_key
    entrypoint: >
      sh -c 'blobhub-worker login --profile prod --no-input --api-key-stdin < /run/secrets/blobhub_api_key &&
      exec blobhub-worker start --profile prod'

secrets:
  blobhub_api_key:
    file: ./blobhub_api_key.secret   # or `external: true` for a secret created with `docker secret create`
```

`exec` in the entrypoint matters: without it, `start` would run as a child of the shell instead of as the
container's own process, and a `docker stop` would have to wait out its default grace period instead of
reaching the worker's own signal handling directly.

## Kubernetes

The same reasoning as Docker applies, with one more wrinkle: an environment variable sourced from a
`secretKeyRef` is set for the life of the container and readable via that container's own `/proc/1/environ`
by anything sharing its process namespace. A volume-mounted `Secret` can be permissioned tighter and doesn't
widen that surface. Project the key to one file and mount it read-only:

```yaml theme={null}
apiVersion: v1
kind: Secret
metadata:
  name: blobhub-api-key
type: Opaque
stringData:
  api-key: bhk_...   # a service account's key -- see /general/service-accounts
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: blobhub-worker
spec:
  replicas: 1
  selector:
    matchLabels: { app: blobhub-worker }
  template:
    metadata:
      labels: { app: blobhub-worker }
    spec:
      containers:
        - name: worker
          image: yourrepo/blobhub-worker:latest
          command: ["sh", "-c"]
          args:
            - >
              blobhub-worker login --profile prod --no-input --api-key-stdin < /etc/blobhub/api-key &&
              exec blobhub-worker start --profile prod
          volumeMounts:
            - name: api-key
              mountPath: /etc/blobhub
              readOnly: true
      volumes:
        - name: api-key
          secret:
            secretName: blobhub-api-key
            items:
              - key: api-key
                path: api-key
```

A mounted `Secret` updates in place when the underlying object changes, but that only matters to a process
that re-reads it; a one-shot `login` at container start won't notice a live rotation. Roll the pod (or run
`login` again) after rotating the key, the same as you would for any other credential baked in at startup.

## systemd's `LoadCredential=`

An environment variable set in a unit file is visible to `systemctl show` and to `/proc/<pid>/environ` for
any local user who can read that process, and it's inherited by every child process the unit spawns.
`LoadCredential=` is systemd's own answer, built for exactly this: it stages the credential in a per-unit,
mode-`0400` directory owned by the service's own user, exposed only to that unit via `$CREDENTIALS_DIRECTORY`
— nowhere else, and not to unrelated processes on the same host.

```ini theme={null}
[Unit]
Description=BlobHub worker
After=network-online.target

[Service]
User=blobhub
LoadCredential=blobhub:/etc/blobhub/api-key.secret
ExecStartPre=/bin/sh -c 'blobhub-worker login --profile prod --no-input --api-key-stdin < "$CREDENTIALS_DIRECTORY/blobhub"'
ExecStart=/usr/local/bin/blobhub-worker start --profile prod
Restart=on-failure

[Install]
WantedBy=multi-user.target
```

`$CREDENTIALS_DIRECTORY` is set for every `Exec*=` line in the unit, so `ExecStartPre` can read it and
`ExecStart` never needs to. `ExecStart=`/`ExecStartPre=` are not run through a shell by default — that's why
the redirect is wrapped in `sh -c '...'` rather than written directly on the `ExecStartPre=` line, where
systemd would pass the literal `<` to the binary as an argument instead of interpreting it.

## Ansible

A playbook is often the thing populating the file the three recipes above expect, but it doesn't have to
write the key to disk on the target at all: `ansible.builtin.command`'s `stdin` parameter feeds a string
straight to the child process's standard input, so the raw key never touches the target's filesystem.

```yaml theme={null}
- name: Provision the BlobHub worker's credential
  ansible.builtin.command:
    cmd: blobhub-worker login --profile prod --no-input --api-key-stdin
    stdin: "{{ blobhub_api_key }}"
  no_log: true
```

`no_log: true` is not optional here. Once the key is a module parameter rather than a file path, Ansible's
own verbose and failure output is the next place it could leak — `no_log` is what keeps it out of both. Keep
`blobhub_api_key` itself in an `ansible-vault`-encrypted variable file; nothing about `login` cares where the
value came from before it reached `stdin`.

<Note>
  Keep `--profile` a literal or a variable you know is always set. `--profile "{{ some_var }}"` rendering to
  an empty string is refused outright on the worker (`PROFILE_NOT_FOUND`) rather than falling through to
  whatever the stored default happens to be — see the
  [warning in Authentication and Profiles](/worker/auth#how-a-credential-is-resolved) for why that's
  deliberate. `blobhub login` on the CLI side does not share this guard and falls through instead, which is
  exactly the asymmetry to design around rather than discover in production.
</Note>

## Confirming what got stored

Both binaries can report the same thing a script would otherwise have to scrape from human-readable lines —
but spelled differently, and shaped differently, which is worth seeing side by side rather than assuming one
mirrors the other.

`--json` is a **root** option on the CLI and a **subcommand** option on the worker:

```bash theme={null}
blobhub --json whoami --profile ci
```

```json theme={null}
{ "profile": "ci", "url": "https://api.blobhub.io/v1", "realtime_url": "wss://realtime.blobhub.io/v1",
  "user": { "id": "usr_01J...", "name": "ci-deploy" }, "scope": "full", "schema_version": "1.0" }
```

```bash theme={null}
blobhub-worker whoami --profile prod --json
```

```json theme={null}
{ "profile": "prod", "label": "prod", "url": "https://api.blobhub.io/v1",
  "realtime_url": "wss://realtime.blobhub.io/v1",
  "user": { "user_id": "usr_01J...", "name": "ci-deploy", "email": "", "recorded_at": "2026-05-27T12:34:56Z" },
  "state_directory": "/home/blobhub/.blobhub-worker/profiles/prod", "running": true }
```

The CLI wraps its payload in an envelope carrying `schema_version`; the worker prints the bare object with no
envelope at all — the same divergence [`blobhub-worker login`](/worker/cli/login) has under `--json`. Neither
ever includes the key itself, here or on any other command. `running` on the worker's payload is the one
field worth watching after a deploy: it tells you whether `start` actually took, not just whether the
credential resolved.

The shapes above are one instance of a wider rule worth carrying into every script built from this page:
**key off the error code and the exit status, never the message text.** The two binaries word the same
condition differently on purpose — see [Error codes](/cli/error-codes) — so a check written against a
sentence is one release, on either side, away from breaking.

## See also

* [Authentication](/cli/auth) and [Authentication and Profiles](/worker/auth) — the full flag reference these
  recipes assume: every source, the prompting rules, and what each binary writes.
* [Service Accounts](/general/service-accounts) — the identity every recipe on this page should be
  provisioning, and its own end-to-end recipe for minting one.
* [Configuration](/cli/configuration) — the credentials file, the precedence ladder, and the `--json`
  envelope in full.
* [Error codes](/cli/error-codes) and [Reference](/worker/reference) — `INPUT_REQUIRED`, `ENV_VAR_NOT_SET`,
  and every other code a scripted `login` can raise.
