GitHub 19 ๐ GitHub Packages and Container Registry (GHCR)
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
| Command | Purpose |
|---|---|
docker login ghcr.io -u USERNAME --password-stdin | Authenticate locally |
docker tag IMAGE ghcr.io/OWNER/NAME:TAG | Tag for GHCR |
docker push ghcr.io/OWNER/NAME:TAG | Push image |
docker pull ghcr.io/OWNER/NAME:TAG | Pull image |
Workflow Permissions
| Permission | Purpose |
|---|---|
contents: read | Checkout code |
packages: write | Push to GHCR |
Package Visibility
| Setting | Effect |
|---|---|
| Private (default) | Only authorized users can pull |
| Public | Anyone can pull anonymously |
Token Scopes
| Scope | Purpose |
|---|---|
read:packages | Pull private images |
write:packages | Push images |
delete:packages | Delete 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
denied: permission_denied | Missing packages: write | Add permissions block |
| Login fails locally | Using account password | Use PAT with write:packages |
| Image not visible | Package default is private | Change visibility in settings |
| Cannot pull from another repo | Package not linked to that repo | Link package to repository |
| Workflow fails on PR | Push attempted on pull request | Add 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
| Item | Value |
|---|---|
| Registry URL | ghcr.io |
| Image naming | ghcr.io/OWNER/IMAGE:TAG |
| Local auth | PAT with write:packages |
| Actions auth | GITHUB_TOKEN with packages: write |
| Default visibility | Private |
| Public pull rate limit | None |
| Package linking | Grants repository admin access |
| Action for tags | docker/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-inGITHUB_TOKENwithpackages: 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.
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!