docker/login-action Username and password required error
The docker/login-action Username and password required error is thrown before any network call, by a three branch check that has two other messages available to it. It chose this one because both values were empty, which is a different problem from one misnamed secret.

What this error means
The step fails in under a second, with no registry named and no connection attempted. Steps before it are green, and the build step after it is skipped. The wording almost never changes, which is why it is easy to miss that it is one of three possible sentences: the action also has Username required and Password required, and the one you got rules the other two out.
Run docker/login-action@v3
Error: Username and password required
##[error]Username and password requiredThree messages, and the one you got
The check lives in loginStandard in src/docker.ts and runs before anything is sent anywhere. It is a ladder, and the order matters, because the first branch that matches wins and the later ones are never reached.
if (!username && !password) {
throw new Error('Username and password required');
}
if (!username) {
throw new Error('Username required');
}
if (!password) {
throw new Error('Password required');
}Common causes
The job never had the secrets, so both interpolations became empty
The common shape, and the one the wording points at. A pull request from a fork gets no repository secrets, a job in a reusable workflow that was called without secrets: inherit gets none either, and both cases empty the whole with: block at once rather than one line of it.
The with: block is misindented and its values are not inputs
YAML will accept a username key that sits one level too deep or beside uses instead of under with, and the action then receives nothing for either name. Both values are empty for the same reason, which is what distinguishes this from a spelling mistake.
Secrets exist at a scope this repository cannot read
An organization secret that was never shared with this repository, or an environment secret in a job that does not name the environment, resolves to an empty string with no warning. When one set of credentials is stored together this way, both halves disappear together.
The login step runs on an event that carries no secrets
Less common but persistent: a workflow that is correct on push and empty on pull_request from a fork. The step is unchanged between the two runs, which is why the file gets read repeatedly and looks fine each time.
How to fix it
Use the sentence to pick the search
- If the message names both values, stop checking secret names and check whether this job receives secrets at all.
- Open the job's event: a fork pull request and an uninherited reusable workflow both empty everything.
- If the message names one value, the other arrived, and a single name or scope is wrong.
Guard the step rather than letting it fail the job
When a job legitimately runs both with and without credentials, test the value through env rather than through the secrets context, which is not available in an if at either job or step level. Copy the secret into env first, then branch on that.
- name: Log in to the registry
if: env.REGISTRY_USER != ''
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}Name the registry explicitly
Set registry even when you think the default is right. It costs one line and it removes the most confusing symptom of the both-empty case, which is a failure that talks about Docker Hub in a workflow that has nothing to do with Docker Hub.
For ECR, expect a different failure entirely
If the registry is ECR, or ecr: true is set, the three sentences cannot be produced. An empty credential there surfaces through the AWS SDK instead. Do not spend time looking for this message in a job that takes that path.
What the wording rules out
Because the branches are exclusive, the sentence you were given is a measurement rather than a generic complaint. The table is the whole of it.
| Sentence thrown | Username value | Password value | Where to look first |
|---|---|---|---|
Username and password required | Empty | Empty | The with: block itself: indentation, a step that lost its inputs, or a job that never had the secrets |
Username required | Empty | Present | The username secret or variable alone |
Password required | Present | Empty | The password secret alone, which is the misnamed-secret case |
The branch only runs for some registries
The ladder sits inside loginStandard, and login decides between that and loginECR before any of it happens. The ECR path is taken when the ecr input is true, or when it is auto and the registry host looks like ECR. On that path the three sentences are unreachable, and an empty credential produces an AWS SDK failure instead, naming a region or a profile.
The other thing login does first is worth knowing for the both-empty case: when the registry input is also empty, there is no registry to name, and the action logs in to Docker Hub by default. A job that meant to authenticate to a private registry, and whose whole with: block failed to resolve, therefore fails while talking about Docker Hub, which it was never configured to use.
Why there is no recorded run on this page
The throw happens before a socket is opened, from two values that were empty in your repository's secret store rather than on any machine. A run of ours would have to deliberately leave our own secrets unset, and the log it produced would be the same single line, carrying no information about which of your names, scopes or triggers was responsible. The sentence above is the one this branch throws, read in the action's source, and the table is that source's own ordering rather than a reproduction.
How to prevent it
- Put the login step in a job that also declares which environment or which secrets it needs, so an empty value is a visible configuration change rather than a silent one.
- Prefer
${{ github.token }}andpackages: writeover a stored credential when the registry is ghcr.io, since there is then nothing to leave unset. - When a workflow must run on fork pull requests, guard the login step rather than letting it take the job red.
- Set
registryon every login step, so the failure names the host you meant.
Frequently asked questions
Why does it say Docker Hub when I am pushing to a private registry?
registry input was empty too. The action defaults to Docker Hub when no registry is given, so a with: block that failed to resolve as a whole takes the default along with everything else. The mention of Docker Hub is a symptom of the same emptiness, not a separate misconfiguration.I only misnamed the password secret, so why does it name both?
Password required. If the message names both, both values were empty when the step started, which points at the block, the event or the secret scope rather than at one name.Can I check whether the secret exists before the login step?
secrets context in an if, because that context is not available in a job-level or step-level if condition. Assign the secret to env on the step or the job and test env.NAME instead. The value is still masked in the log; only its emptiness is observable.Does the message differ between login-action versions?
loginStandard across the v2 and v3 majors. What has moved around them is the dispatch above: which registries are treated as ECR, and the addition of a Docker Hub OIDC path that replaces the username and password before the ladder sees them.