GitHub Actions: CI/CD Workflows From First Run to Secure Deployments · Pipelines, Deployments and Security

Containers and Releases: Publish Images and Tagged Versions

Build and push a Docker image to GitHub Container Registry, and create a GitHub release when a version tag is pushed.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A workflow that builds a Docker image on every push to main, tags images from the Git tag on releases, pushes to ghcr.io, and creates a GitHub release with generated notes.

The idea

  • docker/login-action signs in to a registry. For ghcr.io use github.actor and secrets.GITHUB_TOKEN, with the packages: write permission.
  • docker/metadata-action produces image tags and labels from the branch, tag or SHA, so you never hand-write them.
  • docker/build-push-action builds with BuildKit and pushes; cache-from/cache-to: type=gha reuses layers between runs.
  • Triggering on push: tags: ["v*"] turns Git tags into releases. The preinstalled GitHub CLI (gh release create) authenticates with the token and needs contents: write.

The code

Build and push an image, then publish a release
name: Release
on:
  push:
    branches: [main]
    tags: ["v*"]

jobs:
  image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  release:
    needs: image
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Create the release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GH_REPO: ${{ github.repository }}
        run: gh release create "$GITHUB_REF_NAME" --generate-notes

Output

actionlint: no problems found
workflow: Release
triggers: push (branches: main; tags: v*)
jobs, in the order they run:
  image: runs on ubuntu-latest | 5 step(s)
  release: runs on ubuntu-latest | 1 step(s) | after image | only if startsWith(github.ref, 'refs/tags/v')

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Missing packages: write. Pushing then fails with a permission denied error.
  • Hand-written tags. Use metadata-action so branch builds, tag builds and SHA builds get consistent tags.
  • Releasing from any push. The if: startsWith(github.ref, ...) condition keeps the release job to version tags.
  • Uppercase image names. Registry names must be lowercase; github.repository can contain capitals for some owners, so lowercase it if needed.

Key points

  • Docker's official actions log in, generate tags, build and push.
  • GITHUB_TOKEN with packages: write pushes to ghcr.io.
  • A tag trigger plus gh release create automates releases. Further reading: GitHub Docs.

Practice questions

Why use docker/metadata-action instead of writing the image tags yourself?

Answer

It derives consistent tags and labels from the Git ref (branch, version tag, SHA), so builds are named correctly without custom shell logic.

How does the workflow decide to create a GitHub release only for version tags?

Answer

The release job has if: startsWith(github.ref, 'refs/tags/v'), so it is skipped for branch pushes and runs only when a tag starting with v was pushed.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?