Automating .NET Deployment: From NuGet Packages to Docker Containers via GitHub Actions

Integrating .NET applications into modern DevOps pipelines requires a nuanced understanding of both the .NET SDK capabilities and the orchestration layers provided by platforms like GitHub Actions. The landscape of publishing .NET artifacts has evolved from simple file deployments to complex containerized workflows and automated package registries. Whether the objective is to deploy a web application to Azure App Service, push a Docker image to the GitHub Container Registry (GHCR), or publish a NuGet package to a public or private feed, the underlying mechanics rely on specific configuration files, environment variables, and workflow triggers. This analysis dissects the technical implementation of these publishing strategies, highlighting the critical differences in authentication, build steps, and configuration profiles required for each scenario.

The Foundation: Azure Web App Deployment

The most traditional approach to deploying .NET applications involves compiling the code and publishing the resulting files to a cloud-hosted web server. This workflow is heavily dependent on the actions/setup-dotnet action and specific environment variables to ensure the correct SDK version is used during the build process.

In a standard Azure deployment scenario, the workflow begins by defining essential environment variables. The DOTNET_VERSION variable is explicitly set to a specific SDK version, such as 6.0.401, ensuring consistency across different runner environments. This version string is then referenced by the actions/setup-dotnet@v3 action to install the precise toolchain required for the project. Simultaneously, the AZURE_WEBAPP_PACKAGE_PATH environment variable is assigned the value . (current directory), which instructs the subsequent deployment action where to locate the published artifacts.

The core of this workflow resides within a single job named publish, which executes on the ubuntu-latest runner. The sequence of operations is critical for a successful deployment:

  • The actions/setup-dotnet@v3 action initializes the .NET SDK using the version specified in the environment variables.
  • The dotnet restore command is executed to download all necessary dependencies and packages defined in the project file.
  • The dotnet build command compiles the source code, checking for errors and generating intermediate assemblies.
  • The dotnet publish command creates the final production-ready files, optimizing them for deployment.
  • The dotnet test command may be invoked to ensure the integrity of the code before deployment, although the order of tests versus publishing can vary based on organizational policy.

Once the artifacts are prepared, the azure/webapps-deploy@v2 action takes over. This action requires two primary inputs: the publish-profile and the package. The publish profile is retrieved from a repository secret named AZURE_PUBLISH_PROFILE, which contains the secure credentials and connection details for the target Azure App Service. This separation of credentials from the workflow code is a fundamental security practice in DevOps, preventing the exposure of sensitive authentication data in the repository history.

Containerizing .NET Applications for GitHub Container Registry

As containerization becomes the standard for application deployment, .NET has introduced SDK-integrated container support, allowing developers to publish Docker images directly using the dotnet publish command without requiring a separate Dockerfile. This approach streamlines the CI/CD pipeline but introduces specific authentication and configuration challenges when targeting GitHub Container Registry (GHCR).

Workflow Configuration and Triggers

A GitHub Actions workflow is defined as a set of instructions triggered by specific events. For container publishing, common triggers include pushes to the main branch or manual triggers via workflow_dispatch. Path filters can be applied to ensure the workflow only runs when changes occur within specific directories, such as PublishGitHubAction/**.

The job definition must explicitly declare permissions for the GITHUB_TOKEN. To push images to GHCR, the job requires packages: write permissions, while contents: read is necessary to check out the repository code.

yaml jobs: publish: runs-on: ubuntu-latest permissions: packages: write contents: read steps: - name: Checkout uses: actions/checkout@v2

SDK Container Support Configuration

The .NET project itself must be configured to support container publishing. This is achieved through a publishProfiles folder containing an XML profile file. This profile defines the container properties, including the base image, target tags, and registry details.

xml <Project> <PropertyGroup> <EnableSdkContainerSupport>true</EnableSdkContainerSupport> <WebPublishMethod>Container</WebPublishMethod> <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:7.0</ContainerBaseImage> <ContainerImageTag>1.0.0</ContainerImageTag> <ContainerRegistry>ghcr.io</ContainerRegistry> <ContainerRepository>laurentkempe/containerplayground</ContainerRepository> </PropertyGroup> <ItemGroup> <ContainerPort Include="80" Type="tcp" /> </ItemGroup> </Project>

In this configuration:
- EnableSdkContainerSupport activates the container build pipeline within the .NET SDK.
- WebPublishMethod is set to Container to indicate the output format.
- ContainerBaseImage specifies the base image, such as mcr.microsoft.com/dotnet/aspnet:7.0.
- ContainerRegistry points to ghcr.io.
- ContainerRepository defines the path using the GitHub username and image name, formatted as username/repository-name.

Authentication and Security Challenges

A critical aspect of publishing to GHCR is authentication. While GitHub Actions provides a GITHUB_TOKEN for secure communication, using this token for container registry access can lead to permission errors if not configured correctly. A common failure mode involves receiving error codes CONTAINER1013 and CONTAINER1001, indicating a "Forbidden" status when attempting to upload blobs to the registry.

text Error: /home/runner/.dotnet/sdk/7.0.403/Containers/build/Microsoft.NET.Build.Containers.targets(201,5): error CONTAINER1013: Failed to push to the output registry: CONTAINER1001: Failed to upload blob using POST https://ghcr.io/v2/laurentkempe/containerplayground/blobs/uploads/; received status code 'Forbidden'.

To resolve this, developers often use the docker/login-action to authenticate with the registry before running the publish command. This action uses the github.actor as the username and the GITHUB_TOKEN secret as the password.

yaml - name: Login to GitHub Container Registry uses: docker/login-action@v1 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}

Using the GITHUB_TOKEN instead of a personal access token (PAT) with broad repo scope enhances security by eliminating the need for long-lived credentials that might grant unnecessary access to the repository. However, it is imperative that the package permissions are correctly set in the repository settings to allow the workflow to write to the container registry.

The final publish step in the workflow then executes the SDK command, referencing the specific publish profile:

yaml - name: Publish and push the container image run: | dotnet publish --os linux --arch x64 -c Release /p:PublishProfile=github

Automating NuGet Package Publishing

Beyond application deployment, .NET developers frequently need to publish libraries and components as NuGet packages. Automated workflows can detect version changes and publish packages to NuGet.org or private feeds, ensuring that package distribution is tied directly to the release process.

The SpringHgui/publish-nuget action provides a streamlined way to handle this automation. The workflow is typically triggered on pushes to the main branch.

yaml name: publish to nuget on: push: branches: - main jobs: publish: name: build, pack & publish runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: publish on version change id: publish_nuget uses: SpringHgui/[email protected] with: PROJECT_FILE_PATH: Core/Core.csproj NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_SOURCE: https://api.nuget.org

Key configuration parameters for NuGet publishing include:

  • PROJECT_FILE_PATH: Specifies the path to the project file relative to the repository root.
  • PACKAGE_NAME: Identifies the NuGet package ID, defaulting to the project name if not specified.
  • VERSION_FILE_PATH and VERSION_REGEX: Allow the action to extract version information from a specific file using a regular expression, enabling dynamic version detection.
  • VERSION_STATIC: Useful for fixed versioning strategies or when using external versioning tools like Nerdbank.GitVersioning.
  • TAG_COMMIT: Enables automatic git tagging upon successful publication, using a format like v* where * is replaced by the actual version.
  • NUGET_KEY: The API key for authenticating with the NuGet server, securely stored as a repository secret.
  • NUGET_SOURCE: The URI of the target NuGet server, defaulting to https://api.nuget.org.

Third-Party Build Actions

For developers who prefer not to manage the granular details of dotnet restore, build, and publish commands individually, third-party actions offer a more abstracted approach. The EasyDesk/action-dotnet-publish action, for example, encapsulates the publishing logic into a single step.

yaml - uses: EasyDesk/action-dotnet-publish@v1 with: path: '<path>' output-dir: '<path-to-output-dir>' build-configuration: Release skip-build: true

This action runs the dotnet publish command and creates the publish assets in the specified output directory. By default, it assumes the build is up to date. If a fresh build is required, the skip-build parameter can be set to false. Alternatively, developers can use companion actions like EasyDesk/action-dotnet-build to handle the compilation step separately before invoking the publish action.

It is important to note that such third-party actions are not certified by GitHub. They are governed by separate terms of service and privacy policies, and users must evaluate the security and reliability of these external dependencies before integrating them into critical production workflows.

Conclusion

The ecosystem for publishing .NET applications via GitHub Actions is rich and varied, offering solutions for every deployment scenario. From the straightforward file-based deployment to Azure App Services using azure/webapps-deploy, to the sophisticated containerized workflows pushing images to GHCR, and the automated version-aware publishing of NuGet packages, each method has distinct configuration requirements.

The transition to SDK-integrated container publishing represents a significant simplification for developers, eliminating the need for separate Dockerfiles while maintaining the flexibility of publish profiles. However, this convenience comes with the responsibility of managing authentication securely, particularly when using GITHUB_TOKEN for container registry access. Understanding the interplay between environment variables, publish profiles, and GitHub Actions permissions is essential for building robust, secure, and efficient CI/CD pipelines. As .NET continues to evolve, these automation patterns will remain fundamental to delivering high-quality software in modern development environments.

Sources

  1. dotnet/docs - dotnet-publish-github-action.md
  2. EasyDesk/action-dotnet-publish
  3. Laurent Kempe - Publish .NET Docker Images Using .NET SDK and GitHub Actions
  4. SpringHgui/publish-nuget

Related Posts