GitHub Actions has a built-in way to push container images, and it’s surprisingly less about building and more about the identity you’re using to push.
Let’s see it in action. Imagine you have a workflow that builds a Docker image and then needs to push it to GitHub Container Registry (GHCR).
name: Build and Push Docker Image
on:
push:
branches:
- main
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # This is key!
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository_owner }}/my-app:latest
The core of this workflow is the docker/login-action and docker/build-push-action. The login-action authenticates your workflow run with GHCR. Notice the username is ${{ github.actor }} (the user or bot that triggered the workflow) and the password is ${{ secrets.GITHUB_TOKEN }}. This GITHUB_TOKEN is automatically provided by GitHub Actions and has permissions to interact with your repository’s packages. The build-push-action then uses these credentials to push the image.
The problem this solves is providing a secure and automated way to store and distribute container images directly from your CI/CD pipeline, linked to your GitHub repository. Instead of a separate artifact registry, GHCR integrates seamlessly, allowing you to manage your code and its containerized build artifacts in one place.
Internally, when docker/login-action runs, it’s essentially executing docker login ghcr.io -u <github_actor> -p <github_token>. The build-push-action then uses this active Docker login session to push the image. The tags parameter is crucial; it defines the full path to your image in GHCR, following the pattern ghcr.io/<repository_owner>/<repository_name>:<tag>.
The permissions block in the job definition is critical. packages: write explicitly grants the GITHUB_TOKEN the necessary permissions to push to GHCR. Without this, the login would succeed, but the push would fail with an authentication error.
What most people don’t realize is that the GITHUB_TOKEN is scoped to the specific workflow run and repository. It’s not a long-lived credential that can be used elsewhere. This makes it incredibly secure for automated pushes. You don’t have to manage separate API tokens or credentials for your registry; GitHub handles it for you.
The next step is often to secure your deployments by pulling these images in another workflow or a separate environment.