, 5 Min. Lesezeit, von

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.

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) und actions/setup-node (v7).

Voraussetzungen

  • Ein GitHub-Repository mit einer .NET-Anwendung
  • Ein Dockerfile im 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/cache mit dem Pfad ~/.nuget/packages spart bei einem typischen Projekt ein bis zwei Minuten Restore-Zeit pro Job
  • Multi-Arch-Images: docker/setup-buildx-action + platforms: linux/amd64,linux/arm64 für ARM-kompatible Images
  • Node.js-Projekte: Das gleiche Muster funktioniert mit actions/setup-node@v7 statt setup-dotnet: npm ci, npm test und npm run build folgen 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).

Lassen Sie uns sprechen

Im kostenlosen Erstgespräch schildern Sie mir Ihre Situation. Ich gebe Ihnen eine erste Einschätzung, wie ich Sie unterstützen kann.

Kostenloses Erstgespräch anfragen