Github Actions General Created: 05 May 2026 Updated: 05 May 2026

GitHub Actions: Building and Pushing Docker Images to GHCR

What is GitHub Actions?

GitHub Actions is a built-in CI/CD (Continuous Integration / Continuous Delivery) platform provided by GitHub. It lets you automate your software workflows — such as building, testing, and deploying your application — directly from your GitHub repository.

Workflows are defined as YAML files placed inside the .github/workflows/ directory of your repository. GitHub automatically detects and runs them based on the events you configure.

Core Concepts

ConceptDescription
WorkflowThe top-level automation file (.yml) that defines when and what to run
EventThe trigger that starts a workflow (e.g. push, pull_request)
JobA group of steps that run on the same machine (runner)
StepA single task inside a job — either a shell command (run) or a reusable action (uses)
ActionA pre-built, reusable unit of work published on the GitHub Marketplace
RunnerThe virtual machine that executes the job (e.g. ubuntu-latest)
SecretAn encrypted variable stored in GitHub, injected into workflows at runtime

What is an "Action"?

An action is a reusable component you reference with the uses keyword. Instead of writing shell scripts from scratch, you use community-maintained or official actions.

- uses: actions/checkout@v4 # clones your repo onto the runner
- uses: actions/setup-dotnet@v4 # installs the .NET SDK
- uses: docker/login-action@v3 # authenticates to a Docker registry

Actions are versioned (e.g. @v4). Always pin to a version to avoid unexpected breaking changes.

What is secrets.GITHUB_TOKEN?

GITHUB_TOKEN is an automatically generated secret created by GitHub at the start of every workflow run. You never need to create it manually.

It provides scoped permissions to interact with the GitHub API — in this case, pushing Docker images to GitHub Container Registry (GHCR).

password: ${{ secrets.GITHUB_TOKEN }}

Permissions are controlled per-job using the permissions block:

permissions:
contents: read # read the repo source code
packages: write # push images to GHCR

GitHub Container Registry (GHCR)

GHCR (ghcr.io) is GitHub's built-in Docker image registry. Images are stored under:

ghcr.io/<owner>/<repository>:<tag>

For example: ghcr.io/Fcakiroglu16/example:latest

It is tightly integrated with GitHub — package visibility follows repository visibility, and authentication uses the same GITHUB_TOKEN.

Docker Image Tagging Strategy

Tags identify different versions of the same image. Using multiple tags is a best practice:

Tag TypeExamplePurpose
latestghcr.io/owner/repo:latestAlways points to the most recent build
Git SHAghcr.io/owner/repo:sha-abc1234Exact traceability — which commit built this image
Dateghcr.io/owner/repo:20260505Chronological grouping by build date

The docker/metadata-action@v5 action generates these tags automatically based on the Git context.

OCI Labels

OCI (Open Container Initiative) labels are metadata embedded inside the Docker image. They follow a standard format:

org.opencontainers.image.title=Example
org.opencontainers.image.source=https://github.com/Fcakiroglu16/Example
org.opencontainers.image.created=2026-05-05T10:30:00Z
org.opencontainers.image.revision=185b3d0...

metadata-action generates these automatically from your GitHub repo context. GHCR reads them to display the linked repository, creation time, and commit on the package page.

Workflow Breakdown

The workflow runs in 8 sequential steps inside a single job:

checkout → setup .NET → restore → build → test → login to GHCR → extract metadata → build & push image

Tests must pass before the Docker image is built. If any test fails, the workflow stops and no image is pushed.

Full Workflow Code

# Name of this GitHub Actions workflow shown in the Actions tab
name: Docker Image CI

# Trigger conditions: run this workflow on push or pull_request events targeting the "main" branch
# Also runs when a Git tag matching "v*" is pushed (e.g. v1.0.0) — used for semver releases
on:
push:
branches: [ "main" ]
tags: [ 'v*' ] # triggers when a version tag like v1.2.3 is pushed
pull_request:
branches: [ "main" ]

# Environment variables shared across all jobs
env:
# GitHub Container Registry hostname
REGISTRY: ghcr.io
# Docker image name derived from the repository owner/name (e.g. owner/repo)
IMAGE_NAME: ${{ github.repository }}

jobs:

# Job name: build — responsible for building and pushing the Docker image
build:

# Use the latest Ubuntu runner provided by GitHub
runs-on: ubuntu-latest

# Grant read access to repo contents and write access to GitHub Packages (GHCR)
permissions:
contents: read
packages: write

steps:
# Step 1: Check out the repository source code onto the runner
- uses: actions/checkout@v4

# Step 2: Set up the .NET SDK so we can restore packages and run tests
- name: Set up .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x' # match the target framework in the .csproj files

# Step 3: Restore NuGet dependencies for all projects in the solution
- name: Restore dependencies
run: dotnet restore

# Step 4: Build the solution (without restoring again) to catch compile errors
- name: Build
run: dotnet build --no-restore

# Step 5: Run all unit/integration tests; fail the workflow if any test fails
- name: Run tests
run: dotnet test --no-build --verbosity normal

# Step 6: Authenticate to GitHub Container Registry so we can push images
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
# Target registry (ghcr.io)
registry: ${{ env.REGISTRY }}
# Use the actor (user or bot) that triggered the workflow as the username
username: ${{ github.actor }}
# Use the auto-generated GITHUB_TOKEN secret for the password
password: ${{ secrets.GITHUB_TOKEN }}

# Step 7: Extract Docker metadata (image tags and OCI labels) based on the Git ref/event
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
# Full image path in the registry (e.g. ghcr.io/owner/repo)
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# Always tag the image as "latest"
type=raw,value=latest
# Tag with the short Git SHA for traceability (e.g. sha-abc1234)
type=sha
# Tag with the current build date (e.g. 20260505)
type=raw,value={{date 'YYYYMMDD'}}

# Step 8: Build the Docker image from the Dockerfile and push it to GHCR
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
# Build context is the root of the repository
context: .
# Path to the Dockerfile relative to the repository root
file: RestAprilEducationRepository/Dockerfile
# Only push the image on a "push" event (not on pull_request)
push: ${{ github.event_name == 'push' }}
# Apply the tags generated by the metadata step (e.g. branch name, semver)
tags: ${{ steps.meta.outputs.tags }}
# Apply the OCI labels generated by the metadata step
labels: ${{ steps.meta.outputs.labels }}

Step-by-Step Explanation

Step 1 — actions/checkout@v4

Clones the repository onto the runner. Without this step, none of the source files would be available.

Step 2 — actions/setup-dotnet@v4

Installs the specified .NET SDK version on the runner. The dotnet-version: '10.0.x' wildcard installs the latest patch of .NET 10.

Step 3 — dotnet restore

Downloads all NuGet packages declared in the .csproj files. This is a prerequisite for building.

Step 4 — dotnet build --no-restore

Compiles the solution. --no-restore skips a redundant restore since Step 3 already did it.

Step 5 — dotnet test --no-build --verbosity normal

Runs all test projects found in the solution. If any test fails, the job fails here and the Docker image is never pushed.

Step 6 — docker/login-action@v3

Authenticates to GHCR using the auto-generated GITHUB_TOKEN. Required before any docker push operation.

Step 7 — docker/metadata-action@v5

Generates image tags and OCI labels from the current Git context. The output is stored in steps.meta.outputs.tags and steps.meta.outputs.labels for use in the next step.

Step 8 — docker/build-push-action@v6

Builds the Docker image using the Dockerfile at the specified path, then pushes it to GHCR. The push condition ensures images are only pushed on actual push events — not on pull requests.


Share this lesson: