Jae's Blog

2025-01-15

Building .NET using GitLab CI/CD

Filed under: DevOps,Tutorials — jae @ 04:02

As I often mention, I use .NET a lot in general, as it’s fairly easy to use, has a huge ecosystem, and has evolved really positively in the past years (long gone are the days of Mono :D).

Another component of this is that .NET projects are incredibly easy to build and publish using GitLab CI/CD.
Today, we’re gonna explore some ways of building and publishing a .NET project using just that.

Docker

Probably the most straightforward, considering a simple Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
COPY . ./builddir
WORKDIR /builddir/

ARG ARCH=linux-x64

RUN dotnet publish --runtime ${ARCH} --self-contained -o output

FROM mcr.microsoft.com/dotnet/runtime:9.0

WORKDIR /app
COPY --from=build /builddir/output .

RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

HEALTHCHECK CMD ["curl", "--insecure", "--fail", "--silent", "--show-error", "http://127.0.0.1:8080"]

ENTRYPOINT ["dotnet", "MyApp.dll"]

Code language: YAML (yaml)

Note: this assumes your app builds to MyApp.dll and has a healtheck endpoint on http://127.0.0.1:8080

Then building the container image itself is really easy:

stages:
  - docker

variables:
  DOCKER_DIND_IMAGE: "docker:24.0.7-dind"

build:docker:
  stage: docker
  services:
    - "$DOCKER_DIND_IMAGE"
  image: "$DOCKER_DIND_IMAGE"
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    - docker buildx create --use
    - docker buildx build
      --platform linux/amd64
      --file Dockerfile
      --tag "$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_NAME%+*}"
      --provenance=false
      --push
      .
    - docker buildx rm
  only:
    - branches
    - tags
Code language: YAML (yaml)

This will build, then publish the image to the GitLab container registry of the repo. It’s possible to also specify a different registry, but kinda useless as the default one is already excellent for most cases.

Regular build / NuGet build

This type of build just requires source itself without much additional configuration.

It will build the software, then either upload the resulting files as an artifact or publish it into the GitLab NuGet registry.

For those two, I can recommend setting up a cache policy like:

cache:
  key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
  paths:
    - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json'
    - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*'
    - '$NUGET_PACKAGES_DIRECTORY'
  policy: pull-push
Code language: YAML (yaml)

And a small restore snippet:

.restore_nuget:
  before_script:
    - 'dotnet restore --packages $NUGET_PACKAGES_DIRECTORY'
Code language: YAML (yaml)

You can also directly specify the build image that you want to use at the top of the CI definition file with, for instance:

image: mcr.microsoft.com/dotnet/sdk:9.0
Code language: YAML (yaml)

The regular build with artifact upload is also really easy:

build:
  extends: .restore_nuget
  stage: build
  script:
    - 'dotnet publish --no-restore'
  artifacts:
    paths:
      - MyApp/bin/Release/**/MyApp.dll
Code language: YAML (yaml)

In this case, we use ** to avoid having to update the path every time we upgrade the .NET version (for instance, .NET 8 will put the build in the net8.0 directory, .NET 9 in net9.0, etc).

Now, we can also build and publish the solution to the NuGet registry:

deploy:
  stage: deploy
  only: 
    - tags
  script:
    - dotnet pack -c Release
    - dotnet nuget add source "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/nuget/index.json" --name gitlab --username gitlab-ci-token --password $CI_JOB_TOKEN --store-password-in-clear-text
    - dotnet nuget push "MyApp/bin/Release/*.nupkg" --source gitlab
  environment: $DEPLOY_ENVIRONMENT
Code language: YAML (yaml)

As seen in this definition, this publish stage will only run on tag pushes, but it’s also possible to generate a version string with the current commit and pushing this as a nightly release.

As an additional step, but not really related to the build itself, I often activate the Secret, SAST and dependencies scanning as it can prevent really obvious mistakes. Doing so is also really trivial:

include:
  - template: Jobs/Secret-Detection.gitlab-ci.yml
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
Code language: YAML (yaml)

In the end, building .NET is extremely trivial.

No Comments

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

CC BY-SA 4.0 2012-2025 Jae Lo Presti