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(andcontents: read). - Log in to
ghcr.iowith${{ github.actor }}and${{ secrets.GITHUB_TOKEN }}. - Build and push tagged at
ghcr.io/<owner>/<image>.
Workflow
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 }}:latestGotchas
- The
GITHUB_TOKENis 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.
# 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-failedWhat usually goes wrong first
- The workflow file must exist on the default branch before scheduled or dispatch triggers appear at all.
GITHUB_TOKENpermissions default to read-only in many organisations. Declare apermissions: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/checkoutgives you depth 1 on a detached HEAD, so anything needing history or a branch name needsfetch-depth: 0.