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.


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.
--- 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 formatReproduced on a Latchkey runner
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.
| Rule | What 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 slash | A 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 included | Generated 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
- Compute the image name in one step, with
${GITHUB_REPOSITORY,,}ordocker/metadata-action. - Publish it as a step output so matrix legs and later jobs read the same value.
- Never lowercase at the point of use, because the next step to be added will forget.
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.
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.
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.
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.
- 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 ;;
esacLowercase 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.
- 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?
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?
Does invalid reference format ever come from the registry?
Is it worth retrying a step that failed with invalid reference format?
Related guides
References
- Docker docs: docker image tag, the reference format
- Distribution registry API: repository name grammar and error codes
- Docker docs: docker image pull, defaults for tag and registry
- konflux-ci/build-definitions#3832: a push step crashing on an empty digest file
- Docker documentation
- Docker build cache
- GitHub Actions documentation