Building container images in .Net 8 without Dockerfiles

Vitor Gomes
5 min readJan 29, 2024

Kubernetes and Docker are not just a tool that orchestrates projects in the Dev, Test, and Prod environments.
The community has been creating quality tools to support our CI/CD processes.

Building Docker Images or converting the projects to containers can be tricky. The SDK .NET 8 introduces a new feature that lets you generate containers using .NET commands.

This new implementation will help us create containers using pipelines such as Microsoft DevOps, GitHub, or Jenkins using the DotNet CLI.

To use this new feature, you will need to include the Microsoft.NET.Build.Containers package in your project. This is what handles everything related to generating the container image and simplifies the deployment to the repository.

These auto-generated containers are quite handy in the pursuit of a smooth CI/CD workflow making the project delivery more convenient. This allows us to focus on adding value to our customers instead of spending time with project boilerplate.

So, let’s dive in:

You need to add the Microsoft.NET.Build.Containers package to the main project. These projects can span a variety of technologies and patterns, such as Rest APIs, GRPC APIs, Azure Functions, AWS Lambda, and Workers. After that, you have to include some tags in the CSPROJ file.

In terms of architecture, the objective here would be to have at least
three projects, with the main step responsible for starting the processing and being controllers, Workers, Functions or Lambdas.

The CSPROJECT needs to add a property shown below. If the Target hasn’t been added to the project referenced on the project that will be a container, it returns an error when it is time to generate the image.

<PropertyGroup>
<ContainerRegistry>ghcr.io</ContainerRegistry>
<ContainerRepository>ghcr.io/xxx.xxx.xxx.xx</ContainerRepository>
<ContainerWorkingDirectory>/bin</ContainerWorkingDirectory>
</PropertyGroup>

Other projects that will be added to the primary project reference must have the tag provided below.

<Target Name="PublishContainer" />

The first step to building the pipeline is adding a tool to help and be responsible for managing the code’s version in the repository.

      - name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0
with:
versionSpec: '5.x'

- name: Determine Version
id: version-git
uses: gittools/actions/gitversion/execute@v0

Next, adding the setup to dotnet and restoring the project, the example below added the pre-release version. Note that for production you want to be using .Net LTS versions instead. For more information, refer to the Support Policy.

      - name: Setup .NET
uses: actions/setup-dotnet@v3
with:
source-url: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json
dotnet-version: 8.0.x
env:
NUGET_AUTH_TOKEN: ${{secrets.TOKEN_GITHUB}}


- name: Restore dependencies
run: dotnet restore

To help improve the quality of code, you can use SonarQube, and if you are using the community version, make sure you have all available updates. With the evolution of .Net, some new features can be identified as issues.

Obs. The SonarQube helped but didn’t do miracles.

      - name: Dotnet Install SonarScanner
run: dotnet tool install --global dotnet-sonarscanner

- name: Dotnet Install Coverlet
run: dotnet tool install --global coverlet.console

- name: SonarScanner
run: dotnet sonarscanner begin /k:${{ vars.SONARQUBE_PROJECT_KEY }} /d:sonar.host.url=${{ vars.SONARQUBE_URL }} /d:sonar.login=${{ secrets.SONAR_TOKEN }} /d:sonar.cs.opencover.reportsPaths=$GITHUB_WORKSPACE/**/coverage.opencover.xml /d:sonar.language=cs /d:sonar.visualstudio.enable=true /d:sonar.verbose=false /v:${{ env.GitVersion_SemVer }}

Then build the applications and execute the unit tests.

      - name: Build
run: dotnet build --no-restore --configuration Release

- name: Test
run: dotnet test --configuration release --logger trx /p:CollectCoverage=true /p:CoverletOutputFormat=opencover

Next, you can execute the end steps of SonarQube, finish the process of publishing the code to sonar, and then later just take the results of the sonar.

    - name: SonarScanner End
run: dotnet sonarscanner end /d:sonar.login=${{ secrets.SONAR_TOKEN }}


- name: Check SonarQube Quality Gate
shell: pwsh
run: |
$token = [System.Text.Encoding]::UTF8.GetBytes("${{ secrets.SONAR_TOKEN }}:")
$base64 = [System.Convert]::ToBase64String($token)

$basicAuth = [string]::Format("Basic {0}", $base64)
$headers = @{ Authorization = $basicAuth }
$result = Invoke-RestMethod -Method Get -Uri ${{ vars.SONARQUBE_URL }}/api/qualitygates/project_status?projectKey=${{ vars.SONARQUBE_PROJECT_KEY }} -Headers $headers
$result | ConvertTo-Json | Write-Host

if ($result.projectStatus.status -eq "OK") {
Write-Host "Quality Gate Succeeded"
}else{
throw "Quality gate failed"
}

If your project architecture contains some things that require building Nuget Packages, below is an example. Below, I do this with the client to represent my API EndPoints and contracts.

      - name: dotnet pack ToolBox.Framework.* ${{ env.GitVersion_SemVer }}"
if: github.event_name != 'pull_request'
run: |
dotnet pack --no-build --configuration Release --include-symbols -p:PackageVersion=${{ env.GitVersion_SemVer }}

- name: Push GitHub Packages
if: github.event_name != 'pull_request'
run:
dotnet nuget push ${{ github.workspace }}/**/*.nupkg --skip-duplicate

This approach needs to be added to the Class Library project. The tag is shown below to the project; you don’t need to create the package; the value is false, and the value is true for the cases the project needs. Therefore, the above step will generate the packages automatically.

<IsPackable>false</IsPackable>

If you don't want to add this manually, you can enable and disable this on the project's properties.

The next step in building the image is to create the authentication with Docker; below is an example of connecting with the GitHub repository for containers. From there, the credentials and the .net SDK are generated, which are used to publish the images.

      - name: Docker Login to GitHub Container Registry
if: github.event_name != 'pull_request'
run:
docker login ghcr.io -u ${{ github.repository_owner }} --password ${{ secrets.TOKEN_GITHUB }}

Then can publish the image to the repository, I set the arch to arm64 in the example below because my Kubernetes cluster runs on a Raspberry Pi. You need to set your current arch, the other parameter I set is the version of the image.

      - name: Dotnet Push GitHub Container Registry
if: github.event_name != 'pull_request'
run: dotnet publish --os linux --arch arm64 /t:PublishContainer -p:ContainerImageTag=${{ env.GitVersion_SemVer }} -c Release

You can create the release version and tag it on GitHub to finish.

      - name: Create Release Feature/BugFix Branch
id: create_release
if: github.event_name != 'pull_request'
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ steps.version-git.outputs.semVer }}
release_name: v${{ steps.version-git.outputs.semVer }}
draft: false
prerelease: github.ref != 'refs/heads/main'

For more information regarding this new functionality that will be in the next release of .Net 8, the best place to look is in the .Net repository; note that there are other ways to configure this which I did not cover here, so refer to the official documentation which is in constant change in readiness for the next .Net release.

To finish

I would like to thank Chet Husk, for being so helpful and for answering all my questions so quickly and Paulo Gomes for helping me with my texts.

--

--