Zum Inhalt springen
← Alle Beiträge
· 4 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.

GitHub ActionsCI/CD.NETDockerGHCRDevOpsContainer

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 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@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 SettingsChange 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/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@v4 statt setup-dotnetnpm 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).

Projekt besprechen