Docker does not store registry credentials itself when a credential helper is configured. It shells out to an external binary. ~/.docker/config.json names it:
{
"auths": {},
"credsStore": "desktop"
}
credsStore: "desktop" means Docker will run docker-credential-desktop. If that binary is not on PATH, every pull, push and login fails immediately, before any network request.
Confirm it
cat ~/.docker/config.json
which docker-credential-desktop || echo "NOT ON PATH"
The helper name is always docker-credential- plus the credsStore value:
credsStore | Binary | Provided by |
|---|---|---|
desktop | docker-credential-desktop | Docker Desktop |
osxkeychain | docker-credential-osxkeychain | Docker Desktop / Homebrew on macOS |
wincred | docker-credential-wincred.exe | Docker Desktop on Windows |
secretservice | docker-credential-secretservice | golang-docker-credential-helpers package |
pass | docker-credential-pass | same package, plus pass and GPG |
ecr-login | docker-credential-ecr-login | amazon-ecr-credential-helper |
How you get here
Uninstalled Docker Desktop. You moved to Colima, Rancher Desktop, Podman or plain Docker Engine. The config stayed behind and still points at desktop.
A copied config in CI. Someone committed ~/.docker/config.json or baked it into an image. The runner has no Docker Desktop, so the helper is missing.
A tool reading your config. minikube, Skaffold, Tilt, Testcontainers and various build tools all read ~/.docker/config.json. They hit this even when docker itself is fine, which makes it look like a problem with the tool.
Homebrew Docker CLI on macOS. brew install docker provides the CLI but not the credential helpers, which come with Docker Desktop.
Fix 1: Remove the helper
The quickest fix, and a security tradeoff worth understanding.
cp ~/.docker/config.json ~/.docker/config.json.bak
Edit it and delete the credsStore line:
{
"auths": {}
}
Then log in again:
echo "$DOCKERHUB_TOKEN" | docker login -u myuser --password-stdin
Docker now writes the credential into config.json itself:
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "bXl1c2VyOmRja3JfcGF0X..."
}
}
}
That auth value is base64, not encryption. Anyone who can read the file has your token:
echo "bXl1c2VyOmRja3JfcGF0X..." | base64 -d
Acceptable on a personal machine or a short-lived CI runner. Not acceptable on a shared host. chmod 600 ~/.docker/config.json at minimum.
Fix 2: Install a helper that suits the machine
On Linux, where a desktop keyring exists:
sudo apt-get install -y golang-docker-credential-helpers
{ "credsStore": "secretservice" }
On a headless server, secretservice needs a running D-Bus session and will not work. pass is the usual alternative:
sudo apt-get install -y pass golang-docker-credential-helpers
gpg --generate-key
pass init "your-gpg-id"
{ "credsStore": "pass" }
On macOS without Docker Desktop:
brew install docker-credential-helper
{ "credsStore": "osxkeychain" }
In CI: do not carry a config
The right answer in a pipeline is to log in during the job rather than shipping a config file.
# GitHub Actions
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
If you must write a config, write one without credsStore:
mkdir -p ~/.docker
cat > ~/.docker/config.json <<JSON
{"auths":{"ghcr.io":{"auth":"$(printf '%s:%s' "$USER" "$TOKEN" | base64 -w0)"}}}
JSON
chmod 600 ~/.docker/config.json
Better still, use credHelpers to scope a helper to one registry, so a missing helper cannot break everything:
{
"credHelpers": {
"123456789012.dkr.ecr.eu-west-1.amazonaws.com": "ecr-login"
}
}
credHelpers is per-registry; credsStore is the global default. Per-registry is the safer shape, because a machine without the ECR helper still works for every other registry.
The Kubernetes variant
A kubectl create secret docker-registry built from a config.json containing credsStore produces a Secret with no actual credential, since the real secret lives in the keychain. Pulls then fail with ErrImagePull and an authentication error, which is a confusing way to discover this.
Always build the Secret from explicit values:
kubectl create secret docker-registry ghcr-creds \
--docker-server=ghcr.io \
--docker-username=myuser \
--docker-password="$GITHUB_TOKEN"
A checklist
cat ~/.docker/config.jsonand readcredsStore.which docker-credential-<value>.- Missing and you do not need it → back up the config, remove the line,
docker loginagain. - Understand that without a helper, credentials are base64 in a plain file.
chmod 600. - Linux desktop →
secretservice. Headless →pass. macOS →osxkeychain. - CI → log in during the job; never commit a config.
- Prefer
credHelpersper registry over a globalcredsStore. - Kubernetes → build pull secrets from explicit values, never from a keychain-backed config.
Frequently Asked Questions
What is credsStore in the Docker config?
It names an external credential helper that Docker delegates credential storage to, rather than keeping the secret itself. The value is a suffix: credsStore: "desktop" makes Docker execute docker-credential-desktop, which must be on PATH. Helpers exist so registry tokens can live in the operating system keychain instead of a plain file. When the named binary is absent, Docker fails every registry operation immediately, before any network activity, which is why the error appears even for a public image.
Is it safe to just delete the credsStore line?
It works, and it downgrades your credential storage. Without a helper Docker writes the username and token into ~/.docker/config.json as base64, which is encoding rather than encryption: anyone who can read the file can decode it with a single command. That is a reasonable tradeoff on a personal laptop or an ephemeral CI runner, and a poor one on a shared build host. At a minimum run chmod 600 ~/.docker/config.json, and prefer installing a suitable helper on any machine other people can access.
Why does minikube or Skaffold hit this when docker works fine?
Because those tools read ~/.docker/config.json directly to obtain registry credentials, and they resolve the helper themselves. If your shell has the helper on PATH but the tool runs with a different environment, or the helper is genuinely gone and docker happens to be using cached credentials, the tool fails while docker appears healthy. It is the same root cause, surfaced by a different consumer of the same file, which is why fixing the config fixes all of them at once.
How should I handle registry credentials in CI?
Log in as a step inside the job, using the platform's official login action or a docker login with the token on stdin, and never commit or bake a config.json. A committed config either leaks a credential or, more often, references a helper the runner does not have. Where a config must be written, generate it in the job without credsStore and chmod 600 it. For cloud registries, per-registry credHelpers such as ecr-login are better than a global setting, since one missing helper then cannot break every other registry.
Why does my Kubernetes image pull secret not work after creating it from my Docker config?
Because if that config uses a credential helper, it contains no actual credential; the secret is in your operating system keychain and the file only holds a pointer. Creating a Secret from it produces one with empty auth data, and the kubelet then pulls anonymously and fails. Build the Secret from explicit values instead, with kubectl create secret docker-registry --docker-server --docker-username --docker-password, which puts the real credential into the Secret where the kubelet can use it.