It always happens at the worst time: you’re on a deploy call, the build is green, and the image push fails because docker login can’t authenticate. Someone says “it worked yesterday” with the confidence of a person who has never met caches, keychains, or enterprise proxies.
Registry login failures are rarely “just credentials.” They’re usually a small civil war between Docker, your OS credential store, the registry’s auth scheme, TLS trust, and whatever corporate networking thought would be fun this quarter. Let’s win that war quickly.
Fast diagnosis playbook (do this first)
This is the triage order that saves time. It’s optimized for getting a failing CI runner or laptop back to pushing images without guessing.
1) Confirm what failed: auth, TLS, or network
- If you see 401 Unauthorized or denied: requested access to the resource is denied: auth/scopes/permissions.
- If you see x509, certificate signed by unknown authority, or tls: handshake failure: trust chain / MITM / SNI.
- If you see timeout, no route, proxyconnect, or EOF: network/proxy/DNS.
- If you see error storing credentials, credentials store is not initialized, or exec: docker-credential-*: credential helper/keychain.
2) Identify which registry endpoint you’re targeting
People often type the wrong hostname (or omit it), silently hitting Docker Hub instead of the private registry. Your terminal won’t judge you; it’ll just fail.
3) Inspect Docker’s effective credential configuration
Start with ~/.docker/config.json. If there’s a credsStore or credHelpers entry, your login is probably failing in the helper, not the registry.
4) Turn on client-side debug for one run
Use DOCKER_CLI_DEBUG=1 or Docker daemon logs if you’re on a host that runs the daemon. Debug output clarifies which helper is being called and which endpoint is used.
5) Reproduce with a simple registry ping
Hit /v2/ with curl. A correct, reachable registry responds with 200 or 401. A wrong path, proxy block, or TLS problem shows up immediately.
How Docker registry auth actually works (the parts that matter)
docker login doesn’t “log into Docker.” It negotiates with a specific registry endpoint, then stores a credential (or token) for later HTTP requests to push/pull. Under the hood, it’s just HTTP with a few conventions.
The registry API and the /v2/ probe
Most registries implement the Docker Distribution (Registry HTTP API V2). Docker probes https://REGISTRY/v2/ to see if it’s alive. The response usually falls into:
- 200 OK: registry allows anonymous access (rare for private).
- 401 Unauthorized with a
WWW-Authenticateheader: registry wants auth; Docker follows the indicated scheme. - 404 or HTML: you’re not talking to a registry endpoint (common behind load balancers or path-based routing).
Bearer token flows vs Basic auth
Many registries use a Bearer token flow: registry answers 401 with a WWW-Authenticate: Bearer ... header pointing to a token service. Docker then requests a token scoped to the repository and operation (pull, push), and replays it as Authorization: Bearer.
Others use Basic auth directly (especially older or simpler deployments). Docker sends Authorization: Basic using credentials from your config/helper.
Scopes are why “login succeeded” can still fail
A successful login just means “I can authenticate to get some token/credential stored.” Push/pull failures are often authorization: the token exists but doesn’t have the correct scope. That’s when you get “requested access to the resource is denied” even though you “logged in.”
One quote worth keeping
Paraphrased idea (John Allspaw): Reliability comes from improving the system, not blaming individuals after something fails.
Interesting facts and short history (useful, not trivia)
- Docker Hub wasn’t always “the default” in people’s minds. Early Docker users ran private registries frequently because Hub’s org controls were minimal compared to today.
- Registry HTTP API V2 replaced V1 partly to support better security and content-addressable images; V1 is effectively dead in modern tooling.
- Credential helpers became standard because storing base64-encoded credentials in plaintext JSON was (and still is) a bad look in audits.
- macOS Keychain integration became a de facto expectation; Docker Desktop leaned into OS-native secure storage early, which shaped workflows.
- Windows credential storage shifted over time as Docker Desktop matured; older setups had more hand-rolled configs and more fragile behavior.
- Token-based auth expanded as registries integrated with SSO/IdPs; “password” often became “personal access token” without people noticing.
- Content Trust (Notary v1) tried to make image signing mainstream; adoption lagged, but it pushed orgs to care about provenance.
- Enterprise proxies and TLS inspection remain the number one “works at home, fails at work” driver for registry logins.
Joke #1: Docker auth isn’t hard. It just has a strong preference for failing during demos.
Credential storage: helpers, keychains, and config.json
Docker stores credentials via one of three patterns:
- Credential helper (preferred): OS keychain, pass, secretservice, wincred, etc.
- Per-registry helper mapping (
credHelpers): different helper for different registries. - Inline auth (
authsinconfig.json): base64username:password. Works. Also works for attackers who can read your home directory.
What Docker actually does when you “store creds”
Docker CLI calls an external binary named like docker-credential-osxkeychain or docker-credential-pass. If that binary is missing, broken, or can’t access the keychain, login fails even if the registry would accept your credentials.
Keychain failures look like registry failures unless you read the error
Errors like error storing credentials usually mean: Docker talked to the helper, and the helper refused. Common reasons include:
- Helper binary not installed or not on
PATH. - Keychain locked (macOS), session bus missing (Linux secretservice), or GPG agent not available (pass).
- Permissions in headless CI where no GUI keyring exists.
- Broken
config.jsondue to trailing commas or manual edits.
Opinionated guidance
- On laptops: use OS keychain helpers. Don’t store plaintext auth in
config.json. - In CI: avoid OS keychains. Use
--password-stdinwith ephemeral tokens, or configure a helper that works headless. - In servers: choose repeatability over “secure by vibes.” A locked keyring at 3 a.m. is not a security posture; it’s a reliability bug.
Practical tasks: commands, output meanings, and decisions (12+)
These are runbook-grade tasks. Each includes the command, representative output, what it means, and what decision to make next.
Task 1: Confirm Docker CLI and context
cr0x@server:~$ docker version
Client: Docker Engine - Community
Version: 26.1.4
API version: 1.45
Go version: go1.22.5
Git commit: 5650f9b
Built: Wed Sep 18 10:21:00 2025
OS/Arch: linux/amd64
Server: Docker Engine - Community
Engine:
Version: 26.1.4
API version: 1.45 (minimum version 1.24)
Go version: go1.22.5
Git commit: 3d2c1f3
Built: Wed Sep 18 10:21:00 2025
OS/Arch: linux/amd64
Meaning: Confirms client/server are present, and you’re not accidentally using a remote daemon through context magic.
Decision: If client exists but server is missing (common on Docker Desktop issues or remote shells), fix the daemon first. If versions are ancient, expect auth/TLS incompatibilities.
Task 2: Verify which registry you’re actually logging into
cr0x@server:~$ docker login
Authenticating with existing credentials...
Login Succeeded
Meaning: With no hostname, this targets Docker Hub. That “success” may be irrelevant if you needed registry.corp.local.
Decision: Always specify the registry: docker login registry.corp.local. If a teammate says “login succeeded,” ask “to which registry?”
Task 3: Inspect Docker credential configuration
cr0x@server:~$ cat ~/.docker/config.json
{
"auths": {
"registry.corp.local": {}
},
"credsStore": "secretservice"
}
Meaning: Docker will call docker-credential-secretservice to store and retrieve creds.
Decision: If you’re in CI or a headless server without a session bus, switch to a workable helper or use a temporary config directory.
Task 4: Confirm the credential helper binary exists
cr0x@server:~$ command -v docker-credential-secretservice
/usr/bin/docker-credential-secretservice
Meaning: Helper binary is installed and on PATH.
Decision: If missing, install it or change credsStore. If present, move on to whether it can access its backend.
Task 5: Test the helper directly (diagnose keyring access)
cr0x@server:~$ printf 'https://registry.corp.local\n' | docker-credential-secretservice get
error getting credentials - err: exit status 1, out: "Cannot autolaunch D-Bus without X11 $DISPLAY"
Meaning: The helper depends on a desktop-ish session (D-Bus + secret service). Headless environment can’t satisfy it.
Decision: In CI/headless, don’t use secretservice. Use a different helper, or skip helpers by using an isolated DOCKER_CONFIG with inline auth (only if you can control file permissions) and ephemeral tokens.
Task 6: Run login with explicit user and password via stdin (avoid shell history)
cr0x@server:~$ printf '%s' "$REGISTRY_TOKEN" | docker login registry.corp.local -u ci-bot --password-stdin
Login Succeeded
Meaning: Authentication succeeded and Docker attempted to store the credential.
Decision: If this fails with “error storing credentials,” your helper/storage is the problem. If it fails with 401, token/permissions are wrong.
Task 7: Turn on Docker CLI debug for one login attempt
cr0x@server:~$ DOCKER_CLI_DEBUG=1 docker login registry.corp.local -u alice --password-stdin <<'EOF'
not-a-real-password
EOF
DEBU[0000] command: docker login registry.corp.local -u alice --password-stdin
DEBU[0000] credentials store: secretservice
Error response from daemon: login attempt to registry.corp.local failed with status: 401 Unauthorized
Meaning: Confirms which credential store is active and that the registry returned a 401 (not a network error).
Decision: Fix credentials/scopes/SSO/token; don’t waste time on DNS/TLS yet.
Task 8: Validate registry reachability and auth scheme with curl
cr0x@server:~$ curl -skI https://registry.corp.local/v2/
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Www-Authenticate: Bearer realm="https://auth.corp.local/token",service="registry.corp.local"
Docker-Distribution-Api-Version: registry/2.0
Meaning: Registry is reachable, TLS handshake succeeded (because we used -k to ignore trust), and it uses Bearer token auth.
Decision: If this times out, it’s network/proxy/DNS. If it returns HTML or 404, you’re hitting the wrong endpoint/path routing. If it errors without -k, you have a trust issue to fix.
Task 9: Re-run curl without -k to catch real TLS failures
cr0x@server:~$ curl -sI https://registry.corp.local/v2/
curl: (60) SSL certificate problem: unable to get local issuer certificate
Meaning: Your OS trust store doesn’t trust the registry’s certificate chain (or a proxy is swapping certs).
Decision: Install the correct CA cert into the Docker daemon trust and/or OS store. Do not “fix” this by making the registry insecure unless you enjoy incident reviews.
Task 10: Check Docker daemon logs for auth and TLS hints (systemd Linux)
cr0x@server:~$ sudo journalctl -u docker --since "10 minutes ago" --no-pager | tail -n 20
time="2026-01-02T09:12:44.112334512Z" level=error msg="Handler for POST /v1.45/auth returned error: login attempt to registry.corp.local failed with status: 401 Unauthorized"
time="2026-01-02T09:12:44.112612981Z" level=info msg="attempting next endpoint for registry.corp.local"
Meaning: Confirms daemon saw the login failure and it’s an auth status (not TCP/TLS).
Decision: If daemon logs mention x509 or handshake errors, go straight to certs. If they mention proxyconnect, go to proxy config.
Task 11: Inspect current proxy settings that Docker might inherit
cr0x@server:~$ env | egrep -i 'http_proxy|https_proxy|no_proxy'
HTTPS_PROXY=http://proxy.corp.local:3128
NO_PROXY=localhost,127.0.0.1,.corp.local
Meaning: Docker CLI uses your environment. Docker daemon may have its own proxy config via systemd drop-ins.
Decision: If the registry isn’t in NO_PROXY, your proxy might be intercepting TLS or blocking token service redirects. Add registry and auth endpoints to NO_PROXY for direct connections where appropriate.
Task 12: Check daemon proxy config (systemd drop-in)
cr0x@server:~$ systemctl show --property=Environment docker
Environment=HTTP_PROXY=http://proxy.corp.local:3128 HTTPS_PROXY=http://proxy.corp.local:3128 NO_PROXY=localhost,127.0.0.1
Meaning: The daemon itself uses the proxy, and NO_PROXY is too small.
Decision: Expand NO_PROXY to include your registry hostnames and internal domains; restart Docker. This fixes “pull works in shell but fails in daemon” class bugs.
Task 13: Validate what credentials Docker thinks it has (without printing secrets)
cr0x@server:~$ docker system info 2>/dev/null | sed -n '1,40p'
Client:
Version: 26.1.4
Context: default
Debug Mode: false
Server:
Containers: 12
Running: 2
Paused: 0
Stopped: 10
Server Version: 26.1.4
Storage Driver: overlay2
Meaning: Not directly about credentials, but confirms which daemon and storage driver you’re using (useful when a “login” is happening on a different host via context).
Decision: If context isn’t default, verify you’re authenticating against the same daemon that will do the pull/push.
Task 14: Force Docker to use a clean config directory (isolate from broken helpers)
cr0x@server:~$ mkdir -p /tmp/docker-clean
cr0x@server:~$ DOCKER_CONFIG=/tmp/docker-clean docker login registry.corp.local -u alice --password-stdin <<'EOF'
correcthorsebatterystaple
EOF
WARNING! Your credentials are stored unencrypted in '/tmp/docker-clean/config.json'.
Login Succeeded
Meaning: This bypasses your normal helper configuration and stores inline auth. Docker warns you because it should.
Decision: If this works, your original issue is credential helper/keychain configuration. Fix that properly; don’t permanently live in /tmp hacks unless it’s CI and you control the lifecycle.
Task 15: Verify repository authorization by attempting a scoped action
cr0x@server:~$ docker pull registry.corp.local/platform/base:latest
latest: Pulling from platform/base
Digest: sha256:7b2c6f25d4f7a2f2c1a0a1a3b6d5f1b9e20f6d2b3b0b7a1a2c3d4e5f6a7b8c9d
Status: Image is up to date for registry.corp.local/platform/base:latest
Meaning: Auth works for pull on that repo. Push may still fail if you lack push scope.
Decision: If pull succeeds but push fails, stop re-logging in. Fix permissions in the registry/IdP/project.
Task 16: Reproduce a push denial (different class than “login failed”)
cr0x@server:~$ docker tag alpine:3.20 registry.corp.local/platform/base:test
cr0x@server:~$ docker push registry.corp.local/platform/base:test
The push refers to repository [registry.corp.local/platform/base]
denied: requested access to the resource is denied
Meaning: You’re authenticated, but not authorized to push to that repo (or the repo name is wrong, or the token is missing scope).
Decision: Check RBAC for that repository/project; validate that the token is allowed to push, not just read.
TLS, CA certs, and the “x509: unknown authority” family
TLS issues are the most common “why does it fail only inside the company network?” complaint. Usually it’s one of three things:
- Your registry uses an internal CA and your machine doesn’t trust it.
- A proxy/inspection box is terminating TLS and re-signing with a corporate CA you haven’t installed (or Docker doesn’t see).
- You’re hitting the wrong name (SNI mismatch) because of DNS split-horizon or load balancer configuration.
Where Docker reads trust from (and where it doesn’t)
On Linux, the Docker daemon maintains its own trust behavior in addition to the OS store. For custom registries, Docker supports per-registry CA bundles under /etc/docker/certs.d/REGISTRY_HOST[:PORT]/ca.crt.
On Docker Desktop (macOS/Windows), there’s an extra layer: the Linux VM that runs the daemon has its own trust store. Sometimes importing certs into the OS keychain isn’t enough unless Docker Desktop syncs them.
Don’t normalize “insecure registries”
Yes, you can configure insecure-registries and move on. No, you shouldn’t unless you’re in a throwaway lab. It disables TLS verification and turns a login problem into a supply-chain problem.
Joke #2: Marking a registry “insecure” is like removing smoke detectors because they’re loud.
Proxies, MITM boxes, and network failure modes
If you’re behind a corporate proxy, assume it’s part of the problem until proven otherwise. Registry auth flows frequently involve redirects between registry and token service. Proxies are famous for breaking exactly that kind of multi-hop interaction.
Classic proxy symptoms
- Login prompts repeat because the token service is blocked and Docker keeps retrying.
- EOF mid-login because a proxy closes idle connections or blocks chunked transfer encoding.
- Works with curl but not Docker because Docker daemon uses different proxy variables than your shell.
- “x509” only at work because TLS interception is enabled on the proxy path.
NO_PROXY is not a suggestion
Put your registry host and token service in NO_PROXY when they are reachable directly. Include both the registry domain and any internal suffixes used by the auth realm. When token services are on a different hostname, missing that from NO_PROXY is a common failure.
DNS split-horizon and “wrong endpoint” failures
Private registries often have internal and external DNS. If your laptop switches networks (VPN on/off), the same hostname can point to different load balancers with different TLS certs and auth realms. Docker caches credentials by hostname, not by “environment.” That’s how you end up logged in to the wrong “same name” registry.
Tokens, 2FA, SSO, and the “password is correct” trap
Modern registries frequently sit behind SSO, enforce 2FA, or require personal access tokens (PATs) instead of passwords. Humans notice this; automation often doesn’t.
When passwords stop working quietly
Many systems allow UI logins with SSO but require tokens for API access. Docker uses the registry API. If your org switched authentication providers, your old password might still work in a browser and fail in docker login. That is not Docker being picky; it’s you using the wrong credential type.
Token scope matters more than “valid token”
A token might be valid but not permitted to push to platform/base. That looks like a Docker problem until you realize the registry is enforcing repository-level permissions. Fix it in RBAC, not by regenerating tokens endlessly.
Device code flows and registry CLIs
Some registries provide their own CLI that performs SSO device-code auth and then configures Docker credentials. That can be fine on laptops. In CI, avoid interactive flows. Use service accounts or robot tokens designed for automation.
Three corporate mini-stories from the trenches
Incident #1: The wrong assumption (the registry wasn’t the registry)
A mid-sized company migrated from a self-hosted registry to a managed one. They kept the same hostname, because changing hostnames “breaks developer workflows,” and everyone nodded like that was a law of physics.
Two weeks later, CI started failing. Developers could still docker login successfully. The error showed up on docker push as “requested access to the resource is denied.” People rotated passwords, regenerated tokens, and tried different accounts. Nothing stuck.
The root cause was more boring: split-horizon DNS plus VPN. On VPN, registry.corp.local resolved to the new managed registry. Off VPN (or from certain runners), it resolved to an old internal load balancer that served a legacy registry endpoint with different auth. Login “succeeded” because credentials existed for the hostname; push failed because the token service realm was different and the stored credential didn’t match.
The fix wasn’t heroic. They created two explicit hostnames: one internal, one external, and forced CI to use one consistently. They also added a small check that curls /v2/ and validates the WWW-Authenticate realm matches expectations. The lesson: reusing hostnames across migrations feels tidy until it becomes a distributed ambiguity machine.
Incident #2: An optimization that backfired (credential helper everywhere)
Another org standardized developer workstations using a configuration management profile. Someone decided every Linux box should use secretservice for Docker credentials, because “it’s more secure than plaintext.” True statement, wrong deployment context.
It worked perfectly on desktops. Then they applied the same profile to build agents running headless Linux. On the next release day, image builds started failing at the login step with errors about D-Bus and missing displays. The registry was fine. The credentials were fine. The helper was not fine.
The incident response was predictably chaotic. People tried to restart Docker, rotated passwords, and blamed the registry vendor. Eventually someone ran the helper directly and got the key message: it couldn’t autolaunch D-Bus in a headless session.
The fix was to stop treating “secure storage” as a universal feature and start treating it as a system dependency. They moved CI to use --password-stdin with short-lived robot tokens and set DOCKER_CONFIG to a workspace directory destroyed after the job. Security improved, reliability improved, and nobody had to SSH into runners to unlock imaginary keychains.
Incident #3: The boring correct practice that saved the day (separate auth verification)
A large enterprise had a policy: every pipeline stage that pushes images must first verify registry reachability and auth realm before running the build. It was not exciting. It annoyed people because it added seconds to jobs.
One quarter, the networking team rotated a proxy configuration. Most outbound HTTPS began traversing a new TLS inspection layer. Developers saw intermittent failures: some pulls worked, some logins failed with x509 errors, and the registry team got paged because of course they did.
The pipelines that had the boring preflight check failed fast with a clear error: curl to /v2/ returned a cert chain signed by a different issuer than expected. The failing jobs stopped before building anything. That cut the blast radius dramatically: no half-built artifacts, no wasted runner time, and no “it fails after 20 minutes” drama.
Because the preflight output included the certificate issuer and the WWW-Authenticate realm, the registry team could prove quickly that the registry hadn’t changed. The fix was to distribute the corporate CA into Docker’s trust store on the affected runners and ensure NO_PROXY bypassed inspection for internal registries. Boring practice. Correct practice. Saved the day.
Common mistakes: symptom → root cause → fix
1) “Login Succeeded” but push says denied
Symptom: docker login succeeds; docker push returns denied: requested access to the resource is denied.
Root cause: Authorization failure (no push scope), wrong repository path, or token has pull-only permissions.
Fix: Verify the exact repo name and project namespace. Confirm RBAC grants push. Use a token intended for CI with correct scopes.
2) “error storing credentials” during login
Symptom: Login fails after entering correct creds; error references storing credentials.
Root cause: Credential helper missing/broken, keychain locked, headless environment lacks secret service, or helper can’t prompt.
Fix: Install the helper, or remove/adjust credsStore for that environment. In CI, use --password-stdin and a clean DOCKER_CONFIG.
3) “x509: certificate signed by unknown authority”
Symptom: Docker errors mention x509 or unknown authority.
Root cause: Internal CA not trusted by daemon; TLS interception; wrong hostname/SNI mismatch.
Fix: Install correct CA cert into Docker trust (/etc/docker/certs.d) and/or Docker Desktop VM trust. Fix DNS or certificate SANs. Avoid insecure registries.
4) Repeated password prompts or “unauthorized” after correct password
Symptom: Docker keeps asking for credentials, or rejects known-good credentials.
Root cause: Registry uses tokens/PATs; 2FA enabled; SSO requires a token, not a password.
Fix: Use PAT/robot token. Confirm token service realm and required scopes. Don’t use interactive SSO flows in CI.
5) Works on laptop, fails on CI runner
Symptom: Developers can login and push; CI cannot.
Root cause: CI uses different proxy, DNS, or has no access to keychain helper; different NO_PROXY.
Fix: Make runner network path explicit. Set daemon proxy config correctly. Use headless-compatible credential handling.
6) Pull works, login fails (or vice versa)
Symptom: Anonymous pulls succeed but login fails, or login succeeds but pulls fail from specific repos.
Root cause: Registry allows anonymous access to some content; or token service has issues; or repo-level permissions differ.
Fix: Test with curl -I /v2/ and a specific pull. Align access policy; fix auth service.
7) Docker Desktop: login issues after OS update
Symptom: After macOS/Windows updates, Docker login errors reference keychain/credential manager.
Root cause: Keychain prompts blocked, credential store corruption, or helper binary mismatch.
Fix: Re-authorize Docker Desktop to access keychain, remove stale entries, or reset credentials in Docker Desktop settings and re-login.
8) “no basic auth credentials” during pull
Symptom: Pull fails with no basic auth credentials.
Root cause: Credentials not found for that exact registry hostname (including port), or config stored under a different key.
Fix: Login to the exact hostname:port used in image references. Check ~/.docker/config.json keys match.
Checklists / step-by-step plan
Checklist A: Fix a developer laptop (macOS/Windows/Linux desktop)
- Confirm the exact registry hostname used in image tags (include port if any).
- Run
curl -I https://REGISTRY/v2/to confirm reachability and see auth scheme. - If TLS fails, install the correct CA and ensure Docker Desktop/daemon trusts it.
- Inspect
~/.docker/config.jsonforcredsStore/credHelpers. - Ensure the helper exists and can access the OS keychain/credential manager.
- Login using
--password-stdin(even on laptops) to avoid accidental shell history leaks. - Test
docker pullanddocker pushon a known-good repo to distinguish auth vs permissions.
Checklist B: Fix CI runners (headless Linux)
- Stop using desktop credential helpers unless you know they work headless.
- Use a short-lived token (robot/PAT) injected as a secret.
- Use
docker login ... --password-stdinwithDOCKER_CONFIGset to a job-scoped directory. - Confirm daemon proxy settings via
systemctl show --property=Environment docker. - Set
NO_PROXYto include registry + token service hostnames. - Install internal CA cert into daemon trust path for the registry.
- Add a preflight step: curl
/v2/and fail fast on wrong realm or cert issuer changes.
Checklist C: Fix a self-hosted registry integration
- Verify
/v2/returns correctDocker-Distribution-Api-Versionand auth headers. - Ensure the auth realm/token service is reachable from clients and runners.
- Check TLS certificate SANs include the hostname clients use.
- Confirm repository permissions map correctly to teams/service accounts.
- Validate load balancer routing isn’t returning HTML/404 for
/v2/. - Test from multiple networks (VPN on/off) to catch split-horizon mismatches.
FAQ
Why does docker login succeed but docker push fails?
Login proves authentication. Push requires authorization for that repository and the push scope. Fix RBAC or token scopes, not the login command.
What’s the difference between credsStore and credHelpers?
credsStore sets a default helper for all registries. credHelpers maps specific registries to specific helpers. Use credHelpers when one registry needs special handling.
Why do I get “error storing credentials” in CI?
You likely configured an OS keychain helper that requires a GUI session (secretservice, keychain). In CI, use --password-stdin and a job-scoped DOCKER_CONFIG, or a helper designed for headless use.
Is it safe to store auth in ~/.docker/config.json?
It’s plaintext-ish (base64) and should be treated as sensitive. On shared systems, it’s a bad idea. In CI with ephemeral workspaces and locked-down permissions, it can be acceptable as a pragmatic tradeoff.
What does “no basic auth credentials” actually mean?
Docker couldn’t find credentials for the exact registry key it derived from the image reference. A common cause is logging into registry.corp.local but pulling from registry.corp.local:5000.
Should I use insecure-registries to fix TLS?
No, not as a routine fix. Install the proper CA or fix the cert chain. Use insecure settings only for isolated test environments where you can accept the risk.
Why does it work with curl but not Docker?
curl uses your user environment and OS trust store. Docker daemon may use different proxy settings and may not trust the same CAs. Test both CLI and daemon contexts.
How do I handle SSO/2FA with Docker login?
Use a personal access token or a service account token designed for API use. Your interactive SSO password is often not valid for registry API authentication.
What’s the quickest way to prove it’s a proxy problem?
Compare curl -I https://REGISTRY/v2/ with and without proxy variables, and check whether the WWW-Authenticate realm or cert issuer changes. If it changes, the proxy is in the middle.
Do I need to restart Docker after changing certs?
On Linux daemon hosts, often yes (or at least reload). On Docker Desktop, sometimes you must restart Docker Desktop for VM trust changes to apply.
Conclusion: next steps you can run today
If registry login is failing, don’t treat it like a mystery novel. Treat it like a system: endpoints, trust, auth scheme, credential storage, and scopes.
- Run a
curl -I https://REGISTRY/v2/and read the status, headers, and TLS behavior. - Inspect
~/.docker/config.jsonand verify the credential helper exists and works in your environment. - Use
--password-stdinand a cleanDOCKER_CONFIGto isolate credential-helper issues fast. - Separate “login succeeded” from “push permitted” by testing pull/push on a known repo and fixing RBAC when needed.
- Stop papering over TLS with insecure registries. Install the right CA and make it deterministic across dev and CI.
Once you’ve done this a few times, you’ll recognize the patterns. And the next time someone says “it worked yesterday,” you’ll have a playbook instead of a debate.