# Docker / GHCR

**If your deployment target is "wherever `docker run` works," installing a package manager, a Go toolchain, or a language SDK just to get one static binary onto the box is friction you shouldn't pay.**

A bare VM, a CI runner, a Nomad job, a Compose stack, a pod that isn't ready for an operator — `ghcr.io/whisper-sec/whisper` is the same `whisper` CLI with nothing to install, a locked digest to pin, and three run-modes (plain CLI, MCP server, egress sidecar) behind one entrypoint.

Container images solve a narrower problem than package managers, but they solve it better for the case that actually dominates agent infrastructure: you don't control the host. A GitHub Action runner, a serverless container platform, a customer's Kubernetes cluster, an air-gapped CI pipeline — none of these let you `apt install` or `brew install` anything, but all of them can pull an OCI image. Whisper ships one multi-arch image that carries the exact same Go binary as every other channel — no separate codebase, no "the Docker build lagged the release," because the image *is* the release: the same CI job that cuts a CLI version bakes it into the container and pushes both in the same run.

## What's in the image

```
ghcr.io/whisper-sec/whisper:<version>     # e.g. :0.128.0
ghcr.io/whisper-sec/whisper:latest        # tracks the newest tagged release
```

- **Multi-arch manifest** — `linux/amd64` and `linux/arm64` under one tag; Docker/containerd pick the right layer for the host automatically, so the same `docker run` works on a Graviton instance, an Apple Silicon dev box (via Docker Desktop's VM), or a plain x86 CI runner.
- **Entrypoint is `whisper` itself** — the image is not a general-purpose Linux base with the binary dropped in; `ENTRYPOINT ["whisper"]` means every `docker run … <args>` maps straight onto `whisper <args>`, so the CLI's own `--help` is the image's `--help`.
- **No package manager inside** — a minimal static base, so there's no `apt`/`apk` surface to patch or scan for CVEs beyond the Go binary itself. Nothing to update but the tag.
- **Anonymous pull** — the `whisper-sec` org packages are public; no `docker login` needed to read.

## Verify the image is what it claims to be

Don't trust a README that says "run this" — check the same things you would for any third-party image: which CPU archs are actually in the manifest, and that the binary inside answers a `--version`.

**With stock tools:**

```sh
# what's actually in the manifest (needs buildx or `docker manifest`)
docker manifest inspect ghcr.io/whisper-sec/whisper:latest | jq '.manifests[].platform'
# -> {"architecture":"amd64","os":"linux"}
#    {"architecture":"arm64","os":"linux"}

# pull and run it — proves the binary executes on your arch
docker run --rm ghcr.io/whisper-sec/whisper --version
# -> whisper 0.128.0 (linux/amd64)

# who signed the manifest push / what digest are you actually running
docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/whisper-sec/whisper:latest
# -> ghcr.io/whisper-sec/whisper@sha256:...
```

**With Whisper:** there's nothing Whisper-specific to run here — verifying an OCI image is a Docker/OCI-spec operation, not a Whisper API, which is the point: the same `docker manifest inspect` / `docker run --version` pattern you'd use on any third-party image is all you need. Pin by digest (`ghcr.io/whisper-sec/whisper@sha256:...`) rather than `:latest` in anything that matters — the tag moves, the digest doesn't.

## Three run-modes behind one entrypoint

The image is one artifact; what it *does* is picked by the arguments after it, exactly like the native binary.

### 1. Plain CLI — one-shot commands

```sh
docker run --rm ghcr.io/whisper-sec/whisper verify 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
# -> DNSSEC chain valid, PTR matches forward, TLSA present — same keyless check as `whisper verify` anywhere else
```

No API key needed for `verify`/`rdap` — see [Verify an agent](/docs/verify) for what the check actually walks (DNSSEC → PTR → forward A/AAAA → TLSA, RFC 4035 / RFC 6698). Mount a key in for the control-plane half:

```sh
docker run --rm -e WHISPER_API_KEY=whisper_live_... ghcr.io/whisper-sec/whisper list
# -> the agents registered under that key
```

### 2. MCP server — stdio, for any MCP-speaking agent host

```sh
docker run -i --rm -e WHISPER_API_KEY=whisper_live_... ghcr.io/whisper-sec/whisper mcp
```

This is the container form of the registry entry `io.github.whisper-sec/whisper` — same tool set (keyless `verify`/`rdap`; keyed `register`/`list`/`policy`/`logs`/`revoke`/`egress-config`), just running as a container instead of a locally-installed binary. Point Claude Code, Zed, or any MCP client's config at `docker run -i --rm ghcr.io/whisper-sec/whisper mcp` as the command instead of a bare `whisper mcp` if you'd rather not install the CLI on the host at all — see [MCP server](/docs/mcp).

### 3. Egress sidecar — the mode Kubernetes wants

Run the image as a long-lived `connect` sidecar; it holds the API key and exposes a loopback SOCKS5 port — exactly the shape the [Kubernetes operator](/docs/kubernetes) injects into a pod.

```sh
docker run -d --name whisper-egress \
  -e WHISPER_API_KEY=whisper_live_... \
  ghcr.io/whisper-sec/whisper connect my-agent --tier socks5
```

Give the app container the *same network namespace* as the sidecar (Docker `--network container:whisper-egress`, or the shared-loopback pod model in Kubernetes) so it reaches the proxy on `localhost`, and point `ALL_PROXY` at it. Traffic then egresses from `my-agent`'s routable `/128` — the same Tier 1.5 mechanism (AnyIP + `IP_FREEBIND`) described in [Connect & egress](/docs/connect), just running as a long-lived container next to your app instead of inline in the same process. The app image needs no Whisper code and never handles the bearer — the sidecar does, and the app only ever sees a loopback proxy.

**With stock tools**, from a container sharing the sidecar's netns:

```sh
docker run --rm --network container:whisper-egress curlimages/curl \
  curl -x socks5h://localhost:1080 -s https://api64.ipify.org
# -> 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
```

**With Whisper**, `whisper run` inside the app container does the same proxy-env wiring plus a pre-flight egress check in one step — see the `whisper connect`/`whisper run` walkthrough in [Connect & egress](/docs/connect).

## Compose — app + sidecar in one file

```yaml
services:
  whisper-egress:
    image: ghcr.io/whisper-sec/whisper:latest
    environment:
      WHISPER_API_KEY: whisper_live_...
    command: ["connect", "my-agent", "--tier", "socks5"]

  agent:
    image: my-agent:latest
    network_mode: "service:whisper-egress"   # share the sidecar's netns → reach it on loopback
    environment:
      ALL_PROXY: socks5h://localhost:1080
    depends_on: [whisper-egress]
```

`docker compose up` gives you an agent process that talks to the world through a routable `/128` without a single line of proxy-handling code in the app image — the `ALL_PROXY` env var is what `curl`, Python's `requests`/`httpx`, Go's `net/http`, and Claude Code already read natively (Node's global `fetch` needs undici's `EnvHttpProxyAgent` wired in — one line, since it doesn't pick up proxy env vars on its own).

## Where this fits next to the operator and the raw CLI

The GHCR image is the *building block*; two other channels consume it rather than duplicate it:

- **[Kubernetes](/docs/kubernetes)** — the `whisper-sec/whisper-operator` controller injects this exact image as a sidecar via a mutating admission webhook, so opted-in pods get egress with a namespace label instead of a hand-written manifest. If you're already running the operator, you don't need the recipes above at all; if you're not on Kubernetes (or not ready to install a CRD), the raw image + Compose pattern above is the same mechanism with none of the cluster machinery.
- **[CLI & one-command setup](/docs/cli)** — if the host *does* let you install a binary (a normal VM, a laptop, a CI image you control), `curl -fsSL https://get.whisper.online/install.sh | sh` or the platform package managers are simpler than pulling a container for a single command. Reach for the image specifically when you can't or don't want to touch the host's package state — CI runners, hardened base images, or anywhere "no extra installed packages" is itself a security requirement.

Every path — native binary, container, or Kubernetes sidecar — is the same two-tier contract: no key still gets `verify`/`rdap` for free (real value, zero friction); a key unlocks `register`/`list`/`policy`/`logs`/`revoke`/egress. The full source for the image build lives in [`whisper-sec/whisper-cli`](https://github.com/whisper-sec/whisper-cli); status of every distribution channel, this one included, is tracked live at [whisper.online/integrations](https://whisper.online/integrations).

## Next

[Kubernetes](/docs/kubernetes) — the operator that injects this image as a sidecar automatically · [CLI & one-command setup](/docs/cli) — the same binary installed directly on a host.
