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.
Aktualisiert am 7. Oktober 2026: .NET 8 erreicht am 10. November 2026 das Supportende, aktuelle LTS-Version ist .NET 10. Außerdem nennt der Beitrag jetzt die aktuellen Hauptversionen von
actions/setup-dotnet(v6) undactions/setup-node(v7).
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@v6 |
| 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@v6
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 und 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 bis 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 (LTS) wird nur noch bis zum 10. November 2026 unterstützt; die aktuelle LTS-Version ist .NET 10 (10.0.x, Support bis November 2028).
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, v6 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@v7stattsetup-dotnet:npm ci,npm testundnpm run buildfolgen derselben Logik
Offizielle Dokumentation:
- GitHub Actions: .NET bauen und testen
- Mit der GitHub Container Registry arbeiten
- docker/build-push-action
- actions/setup-dotnet
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).