In deze tutorial leren we hoe je een container image automatisch kunt deployen naar GitHub Container Registry (GHCR) met GitHub Actions.
GitHub Container Registry (ghcr.io) is de container registry van GitHub, vergelijkbaar met Docker Hub maar met een aantal voordelen:
Onder het tabblad Packages op je GitHub Profiel kan je al je gepubliceerde container images zien:
URL: https://github.com/USERNAME?tab=packages (vervang USERNAME door je GitHub gebruikersnaam)

Om in te loggen op GHCR heb je een Personal Access Token (PAT) nodig van GitHub.
Klik rechtsboven op je GitHub gebruikersnaam → Settings in de dropdown

In het menu links, scroll helemaal naar beneden en klik op Developer settings


Configureer je token als volgt:
Note: Geef je token een beschrijvende naam (bijvoorbeeld: GHCR_ACCESS_TOKEN)
Expiration: Kies een expiration date
Select scopes: Selecteer de volgende permissies:
write:packages - Upload packages naar GitHub Package Registryread:packages - Download packages van GitHub Package Registrydelete:packages - Delete packages van GitHub Package Registry (optioneel)
Voorbeeld token: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Nu gaan we inloggen op GHCR via de terminal:
Methode 1: Via command line (minder veilig)
docker login ghcr.io --username YOUR_GITHUB_USERNAME --password YOUR_PAT_TOKEN
Voorbeeld:
docker login ghcr.io --username MilanVives --password ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Output:
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your credentials are stored unencrypted in '/Users/milan/.docker/config.json'.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/
Login Succeeded
Methode 2: Via stdin (veiliger) ✅ Aanbevolen
echo "YOUR_PAT_TOKEN" | docker login ghcr.io --username YOUR_GITHUB_USERNAME --password-stdin
Belangrijk: Images voor GHCR moeten een specifieke naamgeving volgen:
Format:
ghcr.io/GITHUB_USERNAME/IMAGE_NAME:TAG
Regels:
ghcr.iolatest)Voorbeelden:
ghcr.io/milanvives/containerizedapp:latest
ghcr.io/milanvives/myapp:v1.0.0
ghcr.io/milanvives/backend:develop
⚠️ Let op: De volledige naam moet in lowercase zijn!
Nu maken we een eenvoudige containerized applicatie om te testen.
Maak een nieuwe directory aan voor je project:
mkdir containerizedapp
cd containerizedapp
script.shMaak een eenvoudig bash script:
#!/bin/bash
echo "=== Hello from GHCR Container ==="
echo "Container is running successfully!"
echo "Built with GitHub Actions ✓"
Maak het executable:
chmod +x script.sh
Dockerfile# Use Alpine Linux as base image (lightweight)
FROM alpine:latest
# Install bash
RUN apk add --no-cache bash
# Set working directory
WORKDIR /app
# Copy script
COPY script.sh .
# Make script executable
RUN chmod +x script.sh
# Run script on container start
ENTRYPOINT ["./script.sh"]
Commando:
docker build -t ghcr.io/YOUR_GITHUB_USERNAME/containerizedapp:latest .
Voorbeeld:
docker build -t ghcr.io/milanvives/containerizedapp:latest .
Output:
[+] Building 3.8s (11/11) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 160B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 1.8s
=> [auth] library/alpine:pull token for registry-1.docker.io 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/5] FROM docker.io/library/alpine:latest@sha256:4b7ce07... 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 85B 0.0s
=> [2/5] RUN apk add --no-cache bash 1.7s
=> [3/5] WORKDIR /app 0.0s
=> [4/5] COPY script.sh . 0.0s
=> [5/5] RUN chmod +x script.sh 0.1s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:f1ff4ce6dbbaab6df8550393274466b68156e1... 0.0s
=> => naming to ghcr.io/milanvives/containerizedapp:latest 0.0s
docker run --rm ghcr.io/milanvives/containerizedapp:latest
Expected output:
=== Hello from GHCR Container ===
Container is running successfully!
Built with GitHub Actions ✓
Commando:
docker push ghcr.io/YOUR_GITHUB_USERNAME/containerizedapp:latest
Voorbeeld:
docker push ghcr.io/milanvives/containerizedapp:latest
Output:
The push refers to repository [ghcr.io/milanvives/containerizedapp]
0d9b595fd133: Pushed
5cecc193ecd1: Pushed
7abcdc4e5fd7: Pushed
a43bfea15c65: Pushed
0e64f2360a44: Pushed
latest: digest: sha256:0d7be30482f3c21c324935453d236520b23f2ef074177ed0529a4b11402a8449 size: 1358
Check je packages online:
URL: https://github.com/YOUR_USERNAME?tab=packages
Voorbeeld: https://github.com/MilanVives?tab=packages

Nu gaan we het build en push process automatiseren met GitHub Actions.
Maak een nieuwe repository aan op GitHub en push je code:
Stap 1: Initialiseer Git Repository
git init
git branch -M main
Stap 2: Voeg Remote Repository Toe
git remote add origin git@github.com:YOUR_USERNAME/containerizedapp.git
Voorbeeld:
git remote add origin git@github.com:MilanVives/containerizedapp.git
Stap 3: Add, Commit, Push
git add .
git commit -m "Initial commit: Add Dockerfile and script.sh"
git push -u origin main
Output:
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 404 bytes | 404.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:MilanVives/containerizedapp.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
Maak de benodigde directories aan:
mkdir -p .github/workflows
Resulterende structuur:
containerizedapp/
├── .github/
│ └── workflows/
│ └── publish-image.yml # GitHub Actions workflow
├── Dockerfile
└── script.sh
We moeten de PAT token als secret toevoegen aan de repository.
Stap 1: Navigeer naar Repository Settings
Ga naar je repository op GitHub:

Stap 2: Secret Aanmaken
GH_PAT (gebruik hoofdletters!)⚠️ Belangrijk:
GH_PAT zijn (hoofdletters)Maak een nieuw bestand aan: .github/workflows/publish-image.yml
Volledige workflow:
name: Build and Push to GHCR
# Trigger: Bij elke push naar main branch
on:
push:
branches:
- main
# Optioneel: Handmatige trigger
workflow_dispatch:
# Environment variables
env:
REGISTRY: ghcr.io
IMAGE_NAME: $
jobs:
build-and-push:
runs-on: ubuntu-latest
# Permissies nodig voor GHCR
permissions:
contents: read
packages: write
steps:
# Stap 1: Checkout code
- name: Checkout repository
uses: actions/checkout@v4
# Stap 2: Login naar GHCR
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: $
username: $
password: $
# Stap 3: Extract metadata (tags, labels)
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: $/$
tags: |
# Tag met branch naam
type=ref,event=branch
# Tag met commit SHA
type=sha,prefix=-
# Tag 'latest' alleen voor main branch
type=raw,value=latest,enable=
# Stap 4: Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Stap 5: Build en Push image
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: $
labels: $
# Cache layers voor snellere builds
cache-from: type=gha
cache-to: type=gha,mode=max
# Stap 6: Output image details
- name: Image digest
run: echo "Image pushed with digest $"
Triggers:
on:
push:
branches: [main]
workflow_dispatch:
main branchworkflow_dispatch)Permissions:
permissions:
contents: read # Lees repository inhoud
packages: write # Schrijf naar GHCR
Environment Variables:
env:
REGISTRY: ghcr.io
IMAGE_NAME: $ # Automatisch: username/repo-name
Metadata Tags:
latest - Voor main branchmain-abc1234 - Branch naam + SHAmain - Branch naamDocker Buildx:
git add .github/workflows/publish-image.yml
git commit -m "Add GitHub Actions workflow for GHCR"
git push origin main
Na de push:

Stap 1: Ga naar Actions Tab
In je repository: Klik op Actions (bovenaan)
Verwachte Output:
Stap 2: Bekijk Logs (bij problemen)
Klik op de workflow run → Klik op de job → Bekijk de logs
URL: https://github.com/YOUR_USERNAME?tab=packages
Je zou nu je nieuwe package moeten zien met:

Test of de image correct werkt:
# Pull de image van GHCR
docker pull ghcr.io/YOUR_USERNAME/containerizedapp:latest
# Run de container
docker run --rm ghcr.io/YOUR_USERNAME/containerizedapp:latest
Expected Output:
=== Hello from GHCR Container ===
Container is running successfully!
Built with GitHub Actions ✓
Error:
Error: denied: permission_denied: write_package
Oplossing:
GH_PAT secret correct is aangemaaktwrite:packages permissiepermissions in workflow file:
permissions:
contents: read
packages: write
Error:
invalid reference format: repository name must be lowercase
Oplossing:
ghcr.io/milanvives/myapp (lowercase)ghcr.io/MilanVives/MyAppMogelijk redenen:
.github/workflows/ directoryVerificatie:
# Check YAML syntax
yamllint .github/workflows/publish-image.yml
# Check file locatie
ls -la .github/workflows/
Error:
Error: Bad credentials
Oplossing:
GH_PAT secret in repository settingsDefault: GHCR packages zijn private
Publiek maken:
https://github.com/users/USERNAME/packages/container/PACKAGE_NAME⚠️ Waarschuwing: Publieke packages kunnen door iedereen gepulled worden!
✅ DO:
read:packages, write:packages)❌ DON’T:
repo of admin rechten als niet nodigSemantic Versioning:
tags: |
type=semver,pattern=
type=semver,pattern=.
type=semver,pattern=
type=sha
Branch-based:
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=-
Optimaliseer je Dockerfile:
# Build stage
FROM alpine:latest AS builder
RUN apk add --no-cache bash
WORKDIR /app
COPY script.sh .
RUN chmod +x script.sh
# Runtime stage (smaller final image)
FROM alpine:latest
RUN apk add --no-cache bash
WORKDIR /app
COPY --from=builder /app/script.sh .
ENTRYPOINT ["./script.sh"]
GitHub Actions Cache:
- name: Build and push
uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max
Voordelen:
Build voor verschillende platformen:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Build multi-arch
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
push: true
Voeg tests toe voor build:
- name: Run tests
run: |
docker build -t test-image .
docker run --rm test-image ./run-tests.sh
Gebruik GHCR image in Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: ghcr.io/username/containerizedapp:latest
imagePullPolicy: Always
imagePullSecrets:
- name: ghcr-secret
Je hebt nu geleerd:
✅ GitHub Personal Access Token (PAT) aanmaken
✅ Docker login naar GHCR
✅ Container images bouwen en taggen voor GHCR
✅ Handmatig images pushen naar GHCR
✅ GitHub Actions workflow maken voor automatische builds
✅ Troubleshooting common issues
✅ Best practices voor security en efficiency
Voordelen van GHCR + GitHub Actions:
Happy containerizing! 🐳