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.

Want structured learning?

Take the full Github-actions course →