Fix: GitHub Actions Docker Build and Push Failing
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix GitHub Actions Docker build and push errors — registry authentication, image tagging, layer caching, multi-platform builds, and GHCR vs Docker Hub setup.
The Error
A GitHub Actions workflow fails when building or pushing a Docker image:
Error: Username and password required
Error response from daemon: Get "https://ghcr.io/v2/": unauthorized: authentication requiredOr the push step fails after a successful build:
denied: requested access to the resource is denied
error parsing HTTP 403 response body: unexpected end of JSON inputOr the image tag is wrong:
invalid argument "myapp:latest" for "-t, --tag" flag:
invalid reference format: repository name must be lowercaseOr caching doesn’t work and every build is slow:
#15 importing cache manifest from ghcr.io/...
#15 ERROR: failed to solve: ...Why This Happens
Docker build/push in CI requires explicit authentication, correct image naming conventions, and proper permissions. Unlike building on your local machine where docker login persists in ~/.docker/config.json, GitHub Actions runners start clean on every run. There is no saved credential, no cached layer, and no pre-authenticated session. Every piece of configuration that your local workflow assumes must be declared explicitly in the workflow YAML.
The authentication model differs by registry. GitHub Container Registry (GHCR) uses the GITHUB_TOKEN that Actions provides automatically, but that token needs the packages: write permission declared in the workflow’s permissions block. Docker Hub uses a separate access token stored as a repository secret. AWS ECR requires an aws-actions/amazon-ecr-login step that exchanges AWS credentials for a temporary Docker login. Each registry has its own naming convention, visibility defaults, and rate-limiting policy.
Specific causes:
- Missing registry login step —
docker pushrequires authentication first. Withoutdocker/login-action, pushes fail with “unauthorized”. - Insufficient permissions for GHCR — GitHub Container Registry requires
packages: writepermission in the workflow, and the token must have the right scope. - Wrong image name format — Docker image names must be lowercase.
${{ github.repository }}can include uppercase letters if the repo owner or name has them. - Missing
GITHUB_TOKENpermissions — in workflows triggered by pull requests from forks,GITHUB_TOKENhas read-only permissions by default. - Cache not configured correctly —
docker/build-push-actionsupports BuildKit cache, but the cache source/destination must be configured explicitly. - Multi-platform build without
buildx— building forlinux/amd64andlinux/arm64requiresdocker/setup-buildx-action.
How Other Tools Handle This
docker/build-push-action is the most popular approach for building Docker images in GitHub Actions, but it is not the only one. Each alternative makes different trade-offs around security, speed, caching, and registry compatibility.
Manual docker build + docker push skips the official actions entirely. You run docker login, docker build -t image:tag ., and docker push image:tag as shell commands in a run: step. This works but loses BuildKit layer caching (unless you manually configure DOCKER_BUILDKIT=1 and --cache-from/--cache-to), metadata generation, and multi-platform support. The main advantage is transparency: every step is a visible shell command with no action-specific abstraction. Use this approach when debugging action-specific issues or when your Dockerfile is simple enough that caching does not matter.
Kaniko builds container images without a Docker daemon. It runs as a container itself, reads the Dockerfile, and pushes layers directly to the registry. In GitHub Actions, you run Kaniko via docker run gcr.io/kaniko-project/executor:latest. The key difference from docker build is that Kaniko does not require privileged mode or the Docker socket. This makes it safer for untrusted builds (like PR builds from forks) because the build process cannot escape the container. The trade-off is slower build times (Kaniko does not cache as aggressively as BuildKit) and occasional compatibility issues with Dockerfile features like RUN --mount=type=cache.
Buildah is a daemonless, rootless image builder that implements the OCI image spec. In GitHub Actions, you use redhat-actions/buildah-build to build the image and redhat-actions/push-to-registry to push it. Buildah supports Dockerfiles (via buildah bud) and a scripting API (buildah from, buildah run, buildah commit) for programmatic image construction. The scripting API is useful for images that are hard to express in a Dockerfile (e.g., images built from the output of another CI step). Buildah does not require a Docker daemon, which avoids the “Docker-in-Docker” problem on self-hosted runners.
ko is purpose-built for Go applications. It compiles a Go binary, layers it onto a distroless base image, and pushes the result to a registry — all in one command (ko build ./cmd/myapp). There is no Dockerfile. In GitHub Actions, you install ko via ko-build/[email protected] and run ko build in a run: step. ko is faster than docker build for Go because it skips the Dockerfile parsing, layer computation, and multi-stage build overhead. The limitation is that ko only works for Go. If your application includes non-Go assets (static files, config), you need to embed them in the Go binary or use a separate approach.
GHCR vs Docker Hub vs ECR/GCR authentication differs in significant ways. GHCR uses GITHUB_TOKEN with packages: write scope, which means no additional secrets are needed for repositories owned by the same account. Docker Hub requires a separate access token stored as a secret, and free accounts are subject to pull rate limits (100 pulls per 6 hours for anonymous, 200 for authenticated). AWS ECR requires aws-actions/configure-aws-credentials followed by aws-actions/amazon-ecr-login, which exchanges IAM credentials for a temporary 12-hour Docker login token. GCR (Google) uses google-github-actions/auth with a service account key or Workload Identity Federation. The choice of registry affects not just the login step but also image naming (ghcr.io/owner/repo vs 123456789.dkr.ecr.region.amazonaws.com/repo vs docker.io/user/repo), retention policies, and vulnerability scanning capabilities.
Fix 1: Complete Working Workflow (GHCR)
A minimal, working workflow for building and pushing to GitHub Container Registry:
name: Build and Push Docker Image
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # Required for GHCR push
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=sha,prefix=sha-
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }} # Don't push on PRs
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxThe docker/metadata-action handles image naming and tagging automatically — it generates tags like main, pr-42, 1.2.3, and sha-abc1234 based on the event.
Fix 2: Complete Workflow for Docker Hub
For Docker Hub instead of GHCR:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} # Use Access Token, not password
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/myapp
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/myapp:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/myapp:buildcache,mode=maxSet up Docker Hub secrets:
- Go to Repository Settings -> Secrets and variables -> Actions
- Add
DOCKERHUB_USERNAME— your Docker Hub username - Add
DOCKERHUB_TOKEN— create an access token at Docker Hub -> Account Settings -> Security
Use an Access Token, not your password. Access tokens can be scoped to read/write and revoked independently of your account password.
Fix 3: Fix Image Name Case and Format
Docker image names must be lowercase. ${{ github.repository }} includes the owner name, which might be mixed case:
# WRONG — might be "MyOrg/MyApp" which is invalid
tags: ghcr.io/${{ github.repository }}:latest
# CORRECT — convert to lowercase
env:
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Build and push
uses: docker/build-push-action@v5
with:
tags: ghcr.io/${{ env.IMAGE_NAME }}:latestBetter approach — use metadata-action which handles this automatically:
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
# metadata-action lowercases the image name automaticallyOr use a step to normalize the name:
- name: Set image name
id: image
run: echo "name=$(echo 'ghcr.io/${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
tags: ${{ steps.image.outputs.name }}:latestFix 4: Fix Permissions for Fork Pull Requests
When a pull request comes from a fork, GITHUB_TOKEN has read-only permissions. This prevents pushing to GHCR. Use the pull_request_target event for fork PRs, or only push on branch pushes:
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Build and push
uses: docker/build-push-action@v5
with:
# Only push when triggered by a push event (not PRs from forks)
push: ${{ github.event_name == 'push' }}
tags: ...For PRs, build but don’t push — this validates the Dockerfile without requiring write access:
- name: Build (no push) for PR validation
if: github.event_name == 'pull_request'
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: ghcr.io/${{ github.repository }}:pr-${{ github.event.number }}
- name: Build and push on main
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latestFix 5: Speed Up Builds with Layer Caching
Without caching, every build downloads base images and reinstalls dependencies from scratch. Configure BuildKit cache:
GitHub Actions cache (recommended — free, integrated):
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=maxRegistry cache (useful for sharing cache between workflows):
- name: Build and push
uses: docker/build-push-action@v5
with:
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=maxOptimize your Dockerfile for caching — put frequently-changing layers last:
# GOOD — dependencies cached separately from source code
FROM node:20-alpine
WORKDIR /app
# 1. Copy dependency manifests (rarely changes)
COPY package*.json ./
RUN npm ci --only=production # Cached unless package.json changes
# 2. Copy source (changes often)
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]Pro Tip: The
mode=maxoption incache-toexports all layers, not just the final stage’s layers. This is essential for multi-stage Dockerfiles where intermediate stages contain the dependency installation. Withoutmode=max, only the final stage is cached, and the dependency install runs from scratch every time.
Fix 6: Multi-Platform Builds
Build for both linux/amd64 (x86 servers) and linux/arm64 (AWS Graviton, Apple Silicon):
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push multi-platform
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=maxNote: Multi-platform builds are significantly slower than single-platform because QEMU emulates non-native architectures. Use matrix strategy for faster parallel builds:
jobs:
build:
strategy:
matrix:
platform: [linux/amd64, linux/arm64]
runs-on: ubuntu-latest
steps:
- name: Build
uses: docker/build-push-action@v5
with:
platforms: ${{ matrix.platform }}
# ...Fix 7: Pass Build Args and Secrets
Pass environment-specific values to the Docker build without hardcoding them:
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
APP_VERSION=${{ github.sha }}
BUILD_DATE=${{ github.run_started_at }}
NODE_ENV=productionFor secrets that shouldn’t appear in image layers, use BuildKit secrets:
- name: Build with secret
uses: docker/build-push-action@v5
with:
context: .
secrets: |
NPM_TOKEN=${{ secrets.NPM_TOKEN }}# Dockerfile — access secret during build only
RUN --mount=type=secret,id=NPM_TOKEN \
NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) \
npm ci
# Secret is NOT stored in the image layerStill Not Working?
Check the workflow permissions section. GHCR requires packages: write explicitly. A missing or misconfigured permissions block defaults to read-only:
jobs:
build:
permissions:
contents: read
packages: write # Must be here for GHCRVerify the image is public or the package visibility matches. New packages created in GHCR are private by default. If downstream systems try to pull without authentication, they’ll get 403. Set visibility in GitHub -> Packages -> your package -> Package settings -> Change visibility.
Check if the secret is masked in logs. GitHub Actions masks secret values in logs, replacing them with ***. If you see *** where the username or token should appear in a command, the secret is set but may have an incorrect value.
Test the login step in isolation:
- name: Test GHCR login
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io \
-u ${{ github.actor }} \
--password-stdin
echo "Login successful"Check if actions/checkout is missing or incomplete:
# Without checkout, the build context is empty — no Dockerfile found
- name: Checkout
uses: actions/checkout@v4
# If you need Git history (for version tagging), add:
# with:
# fetch-depth: 0A missing actions/checkout step is a surprisingly common oversight, especially in workflows copied from examples that assume a different trigger event.
Verify the Dockerfile path if it is not in the repo root:
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./services/api
file: ./services/api/Dockerfile
# 'context' sets the build context directory
# 'file' specifies the Dockerfile path relative to the repo rootFor related CI/CD issues, see Fix: GitHub Actions Permission Denied, Fix: AWS ECR Authentication Failed, Fix: Docker Build Arg Not Available, and Fix: GitHub Actions Process Completed with Exit Code 1.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Docker Multi-Platform Build Not Working — buildx Fails, Wrong Architecture, or QEMU Error
How to fix Docker multi-platform build issues — buildx setup, QEMU registration, --platform flag usage, architecture-specific dependencies, and pushing multi-arch manifests to a registry.
Fix: GitHub Actions Artifacts Not Working — Upload Fails, Download Empty, or Artifact Not Found
How to fix GitHub Actions artifact issues — upload-artifact path patterns, download-artifact across jobs, retention days, artifact name conflicts, and the v3 to v4 migration.
Fix: GitHub Actions Secret Not Available — Environment Variable Empty in Workflow
How to fix GitHub Actions secrets that appear empty or undefined in workflows — secret scope, fork PR restrictions, environment protection rules, secret names, and OIDC alternatives.
Fix: GitHub Actions Matrix Strategy Not Working — Jobs Not Running or Failing
How to fix GitHub Actions matrix strategy issues — matrix expansion, include/exclude patterns, failing fast, matrix variable access, and dependent jobs with matrix outputs.