Skip to content
Latchkey

How to Push to GHCR With the Built-In Token in GitHub Actions

GHCR accepts the job GITHUB_TOKEN, so pushing images needs no stored registry password.

For GitHub Container Registry, log in with the short-lived GITHUB_TOKEN and packages: write permission. There is no static registry credential to manage; for Docker Hub you still need an access token secret.

Steps

  • Grant the job packages: write (and contents: read).
  • Log in to ghcr.io with ${{ github.actor }} and ${{ secrets.GITHUB_TOKEN }}.
  • Build and push tagged at ghcr.io/<owner>/<image>.

Workflow

.github/workflows/ci.yml
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

Gotchas

  • The GITHUB_TOKEN is per-run and expires when the job ends, so nothing to rotate.
  • Docker Hub has no OIDC login; use a scoped access-token secret there instead of your password.
  • For cross-repo GHCR pushes, the automatic token may lack scope; use a scoped token or app.

Verify it actually works

A workflow that runs is not a workflow that works. Confirm the behaviour on a real event rather than on a manual dispatch, because trigger conditions, permissions, and context values all differ between the two.

Terminal
# 1. validate the file before pushing
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color

# 2. trigger the real event, not workflow_dispatch
git commit --allow-empty -m "ci: verify trigger" && git push

# 3. watch it and read the conclusion, not just the colour
gh run watch
gh run view --log-failed

What usually goes wrong first

  • The workflow file must exist on the default branch before scheduled or dispatch triggers appear at all.
  • GITHUB_TOKEN permissions default to read-only in many organisations. Declare a permissions: block listing every scope the job needs.
  • Fork pull requests get a read-only token and no access to secrets, regardless of workflow configuration.
  • actions/checkout gives you depth 1 on a detached HEAD, so anything needing history or a branch name needs fetch-depth: 0.

Frequently asked questions

How do I push to GHCR With the Built-In Token in GitHub Actions?
For GitHub Container Registry, log in with the short-lived GITHUB_TOKEN and packages: write permission. There is no static registry credential to manage; for Docker Hub you still need an access token secret.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card