Pushing to ghcr.io

Authenticating to ghcr.io and Pushing BookNest There

BookNest's repository from GitHub gets the Dockerfile and .dockerignore of Multi-Stage and BuildKit and a workflow that runs on demand, on version tags, and, without pushing, on pull requests that touch the image:

.github/workflows/publish-image.yml: build and push with GITHUB_TOKENYAML
# Builds BookNest's API image and pushes it to ghcr.io with the workflow's own token.
name: Publish image
on:
  workflow_dispatch:
  push:
    tags: ["v*"]
  pull_request:
    paths: [Dockerfile, .dockerignore, .github/workflows/publish-image.yml]
permissions:
  contents: read
  packages: write
jobs:
  image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: docker/setup-buildx-action@v4
      - uses: docker/login-action@v4
        if: github.event_name != 'pull_request'
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v6
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha
            type=semver,pattern={{version}}
            type=raw,value=latest,enable={{is_default_branch}}
      - uses: docker/build-push-action@v7
        with:
          target: runtime
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

metadata-action derives the tags (sha-<commit>, 1.4.0 from a v1.4.0 tag, latest on the default branch) and OCI labels; build-push-action builds the runtime stage. A ruleset requires pull requests for main, so the files arrive on a branch:

Adding the workflow on a branch and merging it through a pull requestShell
git switch -q -c ch4-ghcr
cp ~/v5-ch4/booknest/Dockerfile ~/v5-ch4/booknest/.dockerignore .
git add Dockerfile .dockerignore .github/workflows/publish-image.yml
git commit -q -m "Publish the API image to ghcr.io from GitHub Actions"
git push -q -u origin ch4-ghcr 2>&1 | grep -v '^remote: *$'
gh pr create --title "Publish the API image to ghcr.io" \
  --body "Adds the Dockerfile from Chapter 4 and a workflow that pushes to ghcr.io."
sleep 15; gh pr checks --watch --interval 10 | tail -1
gh pr merge --squash --delete-branch
Output
remote: Create a pull request for 'ch4-ghcr' on GitHub by visiting:
remote:      https://github.com/binarybehemoth/booknest/pull/new/ch4-ghcr
https://github.com/binarybehemoth/booknest/pull/14
image   pass    24s     https://github.com/binarybehemoth/booknest/actions/runs/36128715748/...
...
Updating 4943462..3bbdf49
Fast-forward
 .dockerignore                       | 21 +++++++++++++++++++
 .github/workflows/publish-image.yml | 40 +++++++++++++++++++++++++++++++++++++
 Dockerfile                          | 37 ++++++++++++++++++++++++++++++++++
 3 files changed, 98 insertions(+)

The pull request's image check built without pushing, and the squash merge landed as 3bbdf49. Now publish from main and pull the result anonymously:

Publishing from main, then pulling the image with no loginShell
gh workflow run publish-image.yml --ref main && sleep 8
RUN=$(gh run list -w publish-image.yml -e workflow_dispatch -L1 \
  --json databaseId -q '.[0].databaseId')
gh run watch "$RUN" --exit-status >/dev/null
gh run view "$RUN" --json conclusion -q .conclusion
gh run view "$RUN" --log | grep -oE 'pushing manifest for [^@]*' | sort -u
docker pull -q ghcr.io/binarybehemoth/booknest:latest
Output
https://github.com/binarybehemoth/booknest/actions/runs/36130127272
success
pushing manifest for ghcr.io/binarybehemoth/booknest:latest
pushing manifest for ghcr.io/binarybehemoth/booknest:sha-ef86458
ghcr.io/binarybehemoth/booknest:latest

One image went out under two tags, and the pull needed no credentials because the package inherited the repository's public visibility:

BookNest's image on the GitHub Container Registry
BookNest's image on the GitHub Container Registry 29