Skip to content
Latchkey LogoLatchkey home

Docker invalid reference format in CI

Docker invalid reference format in GitHub Actions is the client rejecting an image name before it opens a connection to anything, because the string does not match the grammar a reference has to obey. Print the reference the command actually received and the cause is usually in it: an uppercase owner, a variable that expanded to nothing, or a tag that arrived with a colon already attached.

Runner log: three malformed image references refused by Docker before any registry call
The recorded run: an uppercase component, an empty tag and a doubled tag, each refused locally. The step exited 1 and no registry was contacted.
Diagram of the reference grammar, where CI breaks it, and what the runner does
The grammar is checked in the client, so this failure never reaches a registry and never clears on a retry. Rules quoted from the distribution spec.

What this error means

A docker build -t, docker tag, docker pull or docker push step fails in under a second, with no registry named and no network activity in the log above it. Two wordings come out of the same check. A name with a capital letter in it names the offending component, as in "repository name (API) must be lowercase". Anything else malformed gets the bare "invalid reference format", and on a docker build the tag is quoted back to you inside an "invalid tag" line, which is the most useful of the three because it shows the string as the shell finally expanded it. The reproduction below ran all three shapes on one runner, and none of them reached a registry.

Actions log, Docker 29.7.2
--- docker pull MyOrg/API:Latest
    invalid reference format: repository name (API) must be lowercase
--- docker pull ghcr.io/latchkey/api:
    invalid reference format
--- docker build -t ghcr.io/latchkey/api:1.4.2: .
ERROR: failed to build: invalid tag "ghcr.io/latchkey/api:1.4.2:": invalid reference format

Reproduced on a Latchkey runner

Run 2026-09-20·Runner latchkey-small·Exit code 1

Docker version 29.7.2, build a7dcaa6
--- docker pull MyOrg/API:Latest
    invalid reference format: repository name (API) must be lowercase
--- docker pull ghcr.io/latchkey/api:
    invalid reference format
--- docker build -t ghcr.io/latchkey/api:1.4.2: .
ERROR: failed to build: invalid tag "ghcr.io/latchkey/api:1.4.2:": invalid reference format
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)

The runner diagnosed the failure and did not retry it; this failure needs the fix below.

The grammar, and what the defaults fill in

A reference is registry, repository, and then either a tag or a digest. The registry spec is strict about the middle part and permissive about the rest, which is why a name that looks fine to you is refused and a name you never finished writing is accepted.

Check yours against these four rules before you change any workflow code. Three of the four are about characters, and the fourth is the one that hides an empty variable from you.

RuleWhat it means for a workflow
A repository component must match [a-z0-9]+(?:[._-][a-z0-9]+)*Lowercase only, and separators have to sit between characters, never at either end
Path components are separated by a forward slashA doubled slash or a trailing slash is not a component, so it is not a valid name
The whole repository name stays under 256 characters, slashes includedGenerated names built from a branch and a commit can run into this
No tag means :latest, and a single-name image means docker.io/library/...An omitted value is filled in silently; an empty one after a colon is not

Common causes

An uppercase letter in the repository part

The most common one in GitHub Actions, because github.repository and github.actor both carry the case a human chose at signup and a repository component has to be lowercase. It is also the friendliest failure of the set: Docker names the offending component, as it did with "(API)" in the run above, so the fix is visible in the error line itself.

A variable that expanded to nothing

In our experience this is the one that costs an afternoon, because the log shows a reference that looks almost right. A skipped step, a misspelled output name or a version file that was not read leaves the colon and drops the tag, and Docker will not guess. The bare "invalid reference format" wording, with no component named, is the sign that you are looking at this one.

A separator that ended up doubled

A tag assembled from a value that already contains a colon, a registry variable that already ends in a slash, or a digest pasted next to a tag, all produce a string with two separators where the grammar allows one. The build wording helps here: it quotes the whole tag back, so a second colon is visible.

A generated name that is too long or has empty components

Names built from a branch, a matrix leg and a commit run long, and the spec caps the repository name at under 256 characters including slashes. Branch names with slashes in them also add path components, and a branch ending in a slash produces an empty component that the grammar rejects.

How to fix it

Lowercase the owner once, then reuse it

  1. Compute the image name in one step, with ${GITHUB_REPOSITORY,,} or docker/metadata-action.
  2. Publish it as a step output so matrix legs and later jobs read the same value.
  3. Never lowercase at the point of use, because the next step to be added will forget.
.github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - id: meta
        uses: docker/metadata-action@v6
        with:
          images: ghcr.io/${{ github.repository }}
      - uses: docker/build-push-action@v7
        with:
          tags: ${{ steps.meta.outputs.tags }}

Fail on an empty tag before Docker sees it

A guard that checks the assembled reference turns a confusing error into a named one, and it runs in milliseconds. Check for an empty value rather than for a valid one: you are looking for the component that is missing, not writing a second grammar.

.github/workflows/ci.yml
TAG="${{ steps.meta.outputs.version }}"
if [ -z "$TAG" ]; then
  echo "::error::no version was produced, check the metadata step"
  exit 1
fi
docker build -t "ghcr.io/acme/api:$TAG" .

Normalize a branch name before it becomes a tag

Branch names carry slashes, and a tag may not. Replace the separator rather than stripping it, so release/1.4 stays readable and stays one component. The same pass removes uppercase and any character the grammar does not allow.

.github/workflows/ci.yml
REF_NAME="${{ github.ref_name }}"
TAG=$(echo "$REF_NAME" | tr "[:upper:]/" "[:lower:]-" | tr -cd "a-z0-9._-")
docker build -t "ghcr.io/acme/api:${TAG:-dev}" .

Read the wording, because it tells you which half is wrong

Three wordings, three diagnoses. A named component means a character problem in the name. A bare "invalid reference format" means something is missing, usually a tag. An "invalid tag" line from a build quotes the whole string, which is the fastest way to see a doubled colon. Print the reference next to the error and the guessing ends.

Terminal
docker pull "$REF" || { echo "reference was: [$REF]"; exit 1; }

Where the bad string comes from in CI

Almost nobody types an invalid reference. A workflow assembles one, out of context values that are correct on their own. The owner half of github.repository carries whatever case the organization was created with, so an org called Acme produces ghcr.io/Acme/api and the build stops on the capital A while the same command works for everyone whose org is lowercase.

The other producer is an empty step output. A tag read from a previous step that was skipped, or a digest file a push step never wrote, expands to nothing, and the colon you wrote survives while the value does not. That is exactly what konflux-ci/build-definitions#3832 reports: a push step reading an empty digest file and crashing with this error rather than saying what was empty.

.github/workflows/ci.yml
- name: Print the reference before using it
  run: |
    REF="ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }}"
    echo "reference: [$REF]"
    case "$REF" in
      *[A-Z]*) echo "uppercase in the reference"; exit 1 ;;
      *:) echo "empty tag"; exit 1 ;;
    esac

Lowercase where the value is produced, not where it is used

The common fix is a shell expansion at the point of use, and it works right up until a second workflow, a matrix leg or a reusable workflow uses the same value without it. Normalize once, publish the result as a step output, and let every later step read the normalized value.

If the image name comes from docker/metadata-action, you already have this: it lowercases the repository part for you. The value worth guarding after that is the tag, because an action cannot invent a version you did not produce.

.github/workflows/ci.yml
- id: name
  run: echo "image=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
- run: docker build -t "${{ steps.name.outputs.image }}:${{ github.sha }}" .

What the runner does about it

Nothing, and that is the honest answer for this one. The recorded run went through a full self-heal round trip: the wrapper posted the failure to the sidecar and the sidecar answered, no repair was applied, and the run ended on the same error with exit 1. Latchkey carries no pattern for a malformed reference, and it should not, because there is no environment change that turns an invalid string into a valid one.

So the guard from the section above belongs in front of the build rather than a retry action around it: it costs a second, and it names the component that is wrong.

How to prevent it

  • Build every image reference in one step and publish it as an output.
  • Default every tag variable, so an unset value cannot become an empty tag.
  • Lowercase the owner in the same place the reference is assembled.
  • Keep the guard cheap: an echo of the reference is worth more than a retry.

Frequently asked questions

Why does Docker say repository name must be lowercase?
Because the registry grammar allows lowercase letters, digits and separators in a repository component and nothing else. A capital letter anywhere in the repository part fails the check, which is why an organization name with a capital letter breaks ghcr.io/Org/api while the same workflow works for a lowercase org. The tag is not affected: tags may carry uppercase.
Why does my image reference only break in CI and not on my laptop?
Because your laptop has the values and the workflow builds them. A tag taken from a step output, a branch name or a digest file is present when you type it by hand and can be empty in a job where the producing step was skipped. konflux-ci/build-definitions#3832 is the same shape: a push step read an empty digest file and reported this error instead of the missing file.
Does invalid reference format ever come from the registry?
No. The check happens in the client before a connection is opened, which the recorded run shows: three references, three refusals, no registry contacted and no authentication attempted. If the log names a registry or a status code, you are looking at a different failure, most likely access or a missing repository.
Is it worth retrying a step that failed with invalid reference format?
No. The string is invalid on every attempt, so a retry buys the queue wait and the startup time again for the same result. On our recorded run the self-heal round trip completed and nothing was repaired, which is the correct outcome: there is no environment change that makes a malformed name valid.

Related guides

References

A bad reference fails in a second. Latchkey runs everything after it at $0.0025/min at 2 vCPU. Start free → 30-day trial · No credit card