GitHub Actions CI-Boilerplate: .NET-App bauen, testen und als Docker-Image nach GHCR pushen
Ein vollständiger GitHub-Actions-Workflow für .NET-Projekte: Build, Test und Docker-Image-Push nach GHCR – in unter 50 Zeilen YAML, ohne externe Dienste.
Continuous Integration beginnt oft mit einer einfachen Frage: Wie stelle ich sicher, dass mein Code nach jedem Merge kompiliert, alle Tests bestehen – und ein fertiges Docker-Image bereitsteht? GitHub Actions beantwortet diese Frage direkt im Repository, ohne externen CI-Server, ohne zusätzliche Logins. Dieses Boilerplate zeigt einen vollständigen Workflow für eine .NET-Anwendung: automatisches Build und Test bei jedem Push oder Pull-Request, und ein Docker-Image, das bei erfolgreichem Merge auf main direkt in die GitHub Container Registry (GHCR) geschrieben wird.
GHCR ist in GitHub integriert und benötigt keinerlei externe Credentials – der GITHUB_TOKEN, den Actions automatisch bereitstellt, reicht für Login und Push. Es gibt keine separate Registry-Konfiguration, kein Docker Hub-Konto, keine CI/CD-Plattform von Drittanbietern. Der folgende Workflow läuft auf einem ubuntu-latest-Runner vollständig innerhalb von GitHub und erzeugt am Ende ein Image unter ghcr.io/<owner>/<repository>, das sofort per docker pull erreichbar ist.
Voraussetzungen
- Ein GitHub-Repository mit einer .NET-Anwendung
- Ein
Dockerfileim Root des Repositories (z. B. als Multi-Stage-Build) - Keine weiteren Secrets oder externen Dienste erforderlich
Wie der Workflow funktioniert
Push auf main / PR geoeffnet
|
v
+-------------------------------+
| Job: build-test-push |
| ubuntu-latest |
| |
| 1. actions/checkout@v7 |
| 2. actions/setup-dotnet@v5 |
| 3. dotnet restore |
| 4. dotnet build |
| 5. dotnet test |
| 6. docker/login-action@v4 |
| 7. docker/build-push@v7 |
+-------------------------------+
|
v
ghcr.io/<owner>/<repo>:<sha>
Der Image-Push in Schritt 7 findet nur bei einem direkten Push auf main statt, nicht bei Pull-Requests. Build und Test laufen in beiden Fällen durch und geben frühzeitig Rückmeldung.
Der vollständige Workflow
Legen Sie die Datei .github/workflows/ci.yml in Ihrem Repository an:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-test-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v7
with:
context: .
push: ${{ github.ref == 'refs/heads/main' }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
Einige Details, die den Unterschied machen:
permissions-Block: Kein optionaler Komfort, sondern Pflicht. Ohne packages: write lehnt GHCR jeden Push ab. Der Fehler ähnelt einem Netzwerkproblem, ist aber ein Berechtigungsfehler – leicht zu übersehen, wenn man den Block als selbstverständlich ignoriert.
push: ${{ github.ref == 'refs/heads/main' }}: Das Image landet nur bei Merges auf main in der Registry, nicht bei jedem Pull-Request. So bleibt die Registry frei von Zwischenständen aus Feature-Branches, während Build und Test trotzdem bei jedem PR auslösen und schnelles Feedback geben.
--no-restore und --no-build: Der build-Schritt ruft kein zweites restore auf, und test kompiliert nicht erneut. Das vermeidet Doppelarbeit und spart bei einem typischen Projekt 30–60 Sekunden Laufzeit.
${{ github.sha }} als Tag: Jeder Commit erhält ein unveränderliches Tag. Kein latest-Überschreiben, vollständige Nachvollziehbarkeit, und ein klarer Bezug zwischen Git-Commit und laufendem Container. latest lässt sich als zweites Tag über eine kommagetrennte tags-Liste ergänzen, aber das SHA-Tag sollte immer dabei sein.
IMAGE_NAME: ${{ github.repository }}: Diese Variable expandiert zu owner/repository-name (mit Schrägstrich) und dient als Image-Name in GHCR. Das Ergebnis ist ghcr.io/owner/repository-name:sha. Möchten Sie einen anderen Image-Namen, ersetzen Sie github.repository durch einen festen String.
dotnet-version: '8.0.x': Ersetzen Sie diese Angabe durch die .NET-Version Ihres Projekts. 8.0.x ist die aktuelle LTS-Version mit Support bis November 2026.
Drei häufige Fallstricke
1. GHCR-Images sind nach dem ersten Push privat. Neue Packages in GHCR sind standardmäßig privat. Wenn externe Systeme – etwa ein Deployment-Server oder ein Kubernetes-Cluster – das Image ziehen sollen, müssen Sie die Sichtbarkeit ändern: GitHub-Seite des Packages → Package Settings → Change visibility → Public. Alternativ können Sie Zugriff für spezifische Repositories oder Nutzer gewähren, ohne das Image öffentlich zu machen.
2. permission_denied beim ersten Push trotz korrektem permissions-Block. Das tritt auf, wenn das Package zuvor außerhalb von Actions – z. B. mit einem lokalen docker push mit einem persönlichen Access Token – erstellt wurde und nicht mit dem Repository verknüpft ist. Lösung: Im Package-Dashboard unter Manage Actions access das Repository hinzufügen und die Berechtigung auf Write setzen. Danach läuft der Workflow durch.
3. Veraltete Action-Versionen aus Tutorials. Viele Anleitungen zeigen noch docker/login-action@v3, docker/build-push-action@v5, actions/setup-dotnet@v4 oder actions/checkout@v4. Die aktuellen Hauptversionen sind v4, v7, v5 und v7 – alle laufen auf dem Node 24 Runtime. Ein kurzer Blick auf die Release-Seite der jeweiligen Action im GitHub Marketplace lohnt sich, bevor man Snippets aus Artikeln übernimmt.
Wie es weitergeht
Dieses Boilerplate ist ein bewusst minimaler Ausgangspunkt. Häufige Erweiterungen in der Praxis:
- Matrix-Strategie: Mehrere .NET-Versionen parallel testen mit
strategy: matrix: dotnet: ['8.0.x', '10.0.x'] - NuGet-Cache:
actions/cachemit dem Pfad~/.nuget/packagesspart bei einem typischen Projekt ein bis zwei Minuten Restore-Zeit pro Job - Multi-Arch-Images:
docker/setup-buildx-action+platforms: linux/amd64,linux/arm64für ARM-kompatible Images - Node.js-Projekte: Das gleiche Muster funktioniert mit
actions/setup-node@v4stattsetup-dotnet–npm ci,npm testundnpm run buildfolgen derselben Logik
Offizielle Dokumentation:
Hinweis: Die Beiträge dieses Blogs werden unter Einsatz von KI erstellt und vor der Veröffentlichung redaktionell geprüft. Die redaktionelle Verantwortung trägt Emre Yurtbay (siehe Impressum).