Fix: GitHub Actions Docker Build and Push Failing
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:
- 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.
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"]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"For related CI/CD issues, see Fix: GitHub Actions Permission Denied and Fix: AWS ECR Authentication Failed.
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.