| | |

GitHub 19 ๐ŸŒ GitHub Packages and Container Registry (GHCR)

โฑ๏ธ Estimated read: 9 min | |

GitHub Packages is a package hosting service integrated directly into GitHub. It provides registries for npm, RubyGems, Maven, Gradle, NuGet, and Docker/OCI container images. The container registry, GHCR (GitHub Container Registry), is the most widely used component because it stores Docker images alongside source code, links them to repositories, and authenticates through the same GITHUB_TOKEN that Actions workflows already use. The distinction matters for the exam: GitHub Packages is the umbrella service, and GHCR is one of its registries, optimized for container images.

Key point: GHCR lives at ghcr.io. Images are named ghcr.io/OWNER/IMAGE:TAG. Authentication for local development requires a Personal Access Token (classic) with write:packages. Authentication inside GitHub Actions uses the built-in GITHUB_TOKEN with packages: write permission granted in the workflow.


Why GitHub Packages and GHCR exist

The registry fragmentation problem. A project’s code lives on GitHub, its CI runs on GitHub Actions, and its issues are tracked in GitHub Issues. Without GitHub Packages, the container images and npm packages produced by that project would live in a separate registryโ€”Docker Hub, npmjs.com, or a private Artifactory instance. Each requires its own authentication, its own access control, and its own billing. GitHub Packages collapses this into the same platform, using the same permissions model and the same token infrastructure .

The token management problem. Docker Hub requires a username and password, or a dedicated access token, stored as a GitHub Secret. GHCR accepts the GITHUB_TOKEN that Actions injects into every workflow run. The only requirement is that the workflow grants packages: write in its permissions block. No secret needs to be created, rotated, or leaked .

The access control problem. GHCR images can inherit permissions from the repository they are linked to. A private repository’s images are private by default. A public repository’s images can be made public. Access can be granted to specific users or teams without managing a separate registry account .

The rate limit problem. Docker Hub imposes pull rate limits on anonymous and free-tier users. GHCR does not impose pull rate limits on public images, and private image storage and bandwidth are currently not billed beyond the repository’s existing GitHub plan . For teams that pull base images frequently in CI, this eliminates a class of intermittent failures.

The trade-off. GHCR is not a general-purpose Docker registry replacement. It does not have the discoverability of Docker Hub, and it does not support anonymous pulls for private images. For public open-source images, Docker Hub remains the standard. For private application images built from GitHub repositories, GHCR is the path of least resistance.


a. Publishing container images to GHCR

Publishing an image requires three steps: authentication, tagging, and pushing. For local development, authentication uses a Personal Access Token (classic) with write:packages scope.

# Create a PAT at https://github.com/settings/tokens/new?scopes=write:packages

# Login to GHCR
echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin

# Tag the image with the GHCR namespace
docker tag myapp:latest ghcr.io/USERNAME/myapp:latest
docker tag myapp:latest ghcr.io/USERNAME/myapp:v1.0.0

# Push the image
docker push ghcr.io/USERNAME/myapp:latest
docker push ghcr.io/USERNAME/myapp:v1.0.0

The first push creates the package. By default, the package visibility is private. To make it public, navigate to the package settings on GitHub and change the visibility. The package can also be linked to a repository for discoverability and access control inheritance .


b. Automating pushes with GitHub Actions

A GitHub Actions workflow can build and push an image to GHCR without any manually created secrets. The workflow requires permissions: packages: write and uses GITHUB_TOKEN for authentication.

name: Build and Push to GHCR

on:
  push:
    branches: [main]
    tags: ['v*']

permissions:
  contents: read
  packages: write

jobs:
  build-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login to GHCR
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: ghcr.io/${{ github.repository }}

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

The metadata-action generates tags automatically: latest on the main branch, v1.2.3 on a tag push, and a commit SHA tag for every build. The build-push-action pushes only when the event is not a pull request, preventing untrusted PRs from publishing images .


c. Access control and visibility

GHCR packages inherit permissions from the repository they are linked to, or can be managed independently. For private images, access is granted to users or teams through the package settings page. The permission levels are read, write, and admin .

A critical detail for CI workflows: when an image is pushed from a workflow, the repository that owns the workflow is automatically granted admin access to the package. This allows the same workflow to delete or manage the package through the REST API. Other repositories do not have this access unless explicitly granted .

For pulling a private image from a different repository or a local machine, a Personal Access Token with read:packages scope is required. The GITHUB_TOKEN of the pulling repository can be used only if the package has been explicitly linked to that repository .


Complete Example Session

# ============================================
# PART 1: CREATE A PERSONAL ACCESS TOKEN
# ============================================

# Navigate to https://github.com/settings/tokens/new?scopes=write:packages
# Select scope: write:packages
# Generate and copy the token.
# ============================================
# PART 2: LOGIN TO GHCR LOCALLY
# ============================================

export CR_PAT=YOUR_TOKEN
echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
# Output: Login Succeeded
# ============================================
# PART 3: TAG AND PUSH AN IMAGE
# ============================================

docker build -t myapp:latest .
docker tag myapp:latest ghcr.io/USERNAME/myapp:latest
docker push ghcr.io/USERNAME/myapp:latest
# Image is pushed to GHCR.
# ============================================
# PART 4: SET PACKAGE VISIBILITY
# ============================================

# Navigate to:
# https://github.com/users/USERNAME/packages/container/myapp/settings
# Change visibility from private to public if desired.
# ============================================
# PART 5: CREATE THE ACTIONS WORKFLOW
# ============================================

# .github/workflows/publish.yml
# Includes permissions: packages: write
# Uses GITHUB_TOKEN for login
# ============================================
# PART 6: TRIGGER THE WORKFLOW
# ============================================

git add .github/workflows/publish.yml
git commit -m "Add GHCR publish workflow"
git push origin main
# The workflow runs and pushes the image automatically.
# ============================================
# PART 7: VERIFY THE PUSH
# ============================================

# Navigate to the repository's Packages section.
# The image appears with its tags and pull command.
# ============================================
# PART 8: PULL THE IMAGE
# ============================================

docker pull ghcr.io/USERNAME/myapp:latest
# ============================================
# PART 9: LINK TO REPOSITORY
# ============================================

# In package settings, link the package to a repository.
# This grants the repository's workflow automatic admin access.

The nine parts trace the GHCR lifecycle: token creation, local authentication, image push, visibility configuration, workflow automation, verification, pulling, and repository linking.


Quick Reference

GHCR Commands

CommandPurpose
docker login ghcr.io -u USERNAME --password-stdinAuthenticate locally
docker tag IMAGE ghcr.io/OWNER/NAME:TAGTag for GHCR
docker push ghcr.io/OWNER/NAME:TAGPush image
docker pull ghcr.io/OWNER/NAME:TAGPull image

Workflow Permissions

PermissionPurpose
contents: readCheckout code
packages: writePush to GHCR

Package Visibility

SettingEffect
Private (default)Only authorized users can pull
PublicAnyone can pull anonymously

Token Scopes

ScopePurpose
read:packagesPull private images
write:packagesPush images
delete:packagesDelete images

Best Practices

โœ… Do This:

permissions:
  packages: write
<!-- Grant explicit permission for GHCR push -->      # โœ…
password: ${{ secrets.GITHUB_TOKEN }}
<!-- Use GITHUB_TOKEN in Actions workflows -->        # โœ…
push: ${{ github.event_name != 'pull_request' }}
<!-- Prevent PRs from pushing images -->             # โœ…

โŒ Don’t Do This:

# Use account password for GHCR login
<!-- Requires a Personal Access Token instead -->     # โŒ
# Forget packages: write permission
<!-- GITHUB_TOKEN has read-only access by default --> # โŒ
# Push from pull requests without restriction
<!-- Untrusted code could publish images -->          # โŒ

Common Pitfalls

PitfallWhy It HappensFix
denied: permission_deniedMissing packages: writeAdd permissions block
Login fails locallyUsing account passwordUse PAT with write:packages
Image not visiblePackage default is privateChange visibility in settings
Cannot pull from another repoPackage not linked to that repoLink package to repository
Workflow fails on PRPush attempted on pull requestAdd condition to skip PRs

Real-World Examples

1. Local Login

echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin

2. Tag Image

docker tag myapp:latest ghcr.io/USERNAME/myapp:latest

3. Push Image

docker push ghcr.io/USERNAME/myapp:latest

4. Pull Image

docker pull ghcr.io/USERNAME/myapp:latest

5. Actions Login

uses: docker/login-action@v4
with:
  registry: ghcr.io
  username: ${{ github.actor }}
  password: ${{ secrets.GITHUB_TOKEN }}

6. Actions Build and Push

uses: docker/build-push-action@v7
with:
  push: true
  tags: ${{ steps.meta.outputs.tags }}

7. Grant Permission

permissions:
  packages: write

8. Change Visibility

Package settings โ†’ Change visibility

9. Link to Repository

Package settings โ†’ Linked repositories

10. Metadata Action

uses: docker/metadata-action@v6

Visual

GHCR Image Path

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Repository: user/myapp                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Workflow builds image                       โ”‚
โ”‚  docker/build-push-action                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  GHCR: ghcr.io/user/myapp:latest             โ”‚
โ”‚  โ”œโ”€โ”€ Tags: latest, sha-abc123, v1.0.0        โ”‚
โ”‚  โ””โ”€โ”€ Linked to repository: user/myapp        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Authentication Paths

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Local Development                           โ”‚
โ”‚  PAT (write:packages) โ†’ docker login         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  GitHub Actions                              โ”‚
โ”‚  GITHUB_TOKEN + packages: write              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Two paths, one registry.


Summary

ItemValue
Registry URLghcr.io
Image namingghcr.io/OWNER/IMAGE:TAG
Local authPAT with write:packages
Actions authGITHUB_TOKEN with packages: write
Default visibilityPrivate
Public pull rate limitNone
Package linkingGrants repository admin access
Action for tagsdocker/metadata-action

Key takeaways:

  • GHCR is part of GitHub Packages. It is the container registry component, alongside npm, Maven, Gradle, NuGet, and RubyGems registries .
  • Authentication differs by context. Local development requires a Personal Access Token (classic) with write:packages. GitHub Actions uses the built-in GITHUB_TOKEN with packages: write .
  • Images are linked to repositories. Linking grants the repository’s workflow admin access to the package and improves discoverability.
  • Public images have no pull rate limits. This is GHCR’s primary advantage over Docker Hub for CI pipelines .
  • Default visibility is private. The first push creates a private package. Visibility must be changed explicitly in the package settings .
  • Pull requests should not push images. The push: ${{ github.event_name != 'pull_request' }} condition prevents untrusted code from publishing .

Remember: GHCR is the registry that lives where your code lives. It uses the same token, the same permissions, and the same repository model. The GITHUB_TOKEN is the key, packages: write is the permission, and ghcr.io/OWNER/IMAGE:TAG is the address. The workflow builds, tags, and pushes. The registry stores, links, and serves. No separate account, no separate billing, no separate authentication.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!