Nowadays with Kubernetes being so popular, building a Docker image is a must thing for CI/CD pipeline. For this kind of pipelines, an artifact is not a simple zip file wich compiled application, but a Docker image pushed to container registry. There is plenty of benefits of this approach but there is also price for this. We need to handle this in our pipelines. Hopefully, this price is not high. And we will explore today how we can build a Docker image for our dotnet core web app on Azure DevOps.

DOCKERFILE

Our app is straightforward. It’s nothing more than scaffolding provided by Visual Studio on creating dotnet core web app. So let’s skip this part and move to DOCKERFILE. We use multi-stage build where in the first stage we use mcr.microsoft.com/dotnet/core/sdk:3.1 image as our build environment to switch later to mcr.microsoft.com/dotnet/core/aspnet:3.1 as our runtime, just to provide a minimal image.

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app

# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out --no-restore

# Build runtime image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "SampleAppForDocker.dll"]

Building Docker image on host agent

We will push our images to Azure Container Registry, but before that we need to create one.

Create first a resource group using az cli:

az group create --name TheCodeManual --location westeurope

Then create Azure container registry:

az acr create --resource-group TheCodeManual --name devopsmanual --sku Basic

I selected Basic tier. Primary differences between Basic and Standard are included storage and number of web hooks. For our puproses Basic is enough.

Now we need to define service connection on Azure DevOps. You will need to navigate to Service connections* under your Project settings. Then click New service connection select Docker Registry and fill form like below:

Adding new ACR service connection

Now we are ready to define our pipeline:

steps:
- task: Docker@2
  displayName: Login to ACR
  inputs:
    command: login
    containerRegistry: devopsmanual-acr

- task: Docker@2
  displayName: Build and Push
  inputs:
    repository: $(imageName)
    command: buildAndPush
    Dockerfile: build-docker-image/SampleAppForDocker/DOCKERFILE
    tags: |
      build-on-agent

- task: Docker@2
  displayName: Logout of ACR
  inputs:
    command: logout
    containerRegistry: devopsmanual-acr

We have here three very simple steps:

  1. Login to Azure Container Registry
  2. Build and push Docker image
  3. Logout from Azure Container Registry

This build took 55 seconds.

Building Docker image on Azure Container Registry

Another approach is to use ACR tasks. But what is ACR task? The codumentation explains this very well:

ACR Tasks is a suite of features within Azure Container Registry. It provides cloud-based container image building for platforms including Linux, Windows, and ARM, and can automate OS and framework patching for your Docker containers. ACR Tasks not only extends your "inner-loop" development cycle to the cloud with on-demand container image builds, but also enables automated builds triggered by source code updates, updates to a container's base image, or timers. For example, with base image update triggers, you can automate your OS and application framework patching workflow, maintaining secure environments while adhering to the principles of immutable containers.

So basicly it will allow us to offload some part of CI steps to ACR. In our case it will be build and push docker image. You may wonder how much does cost. At the momen of writing this text it is $0.0001/second per CPU.

Our pipeline definition for this approach has single step which calls az acr build.

steps:
- task: AzureCLI@2
  displayName: Azure CLI
  inputs:
    azureSubscription: rg-the-code-manual
    scriptType: pscore
    workingDirectory: $(Build.SourcesDirectory)/build-docker-image/SampleAppForDocker/
    scriptLocation: inlineScript
    inlineScript: |
      az acr build --image sampleappfordocker:build-on-acr --registry devopsmanual --file DOCKERFILE .

This build took 94 seconds to create and publish the image. It is almost twice longer, however for such simple projects we should not pay too much attention to these results. The purpose here was to show how we can build image and for benchmarking we should do more than this.

Building Docker image on host agent with Build Kit

Looking at options how we can build Docker images on Azure DevOps I found a Build Kit project. You may ask what it is. Let me cyte the offical site:

BuildKit is a toolkit for converting source code to build artifacts in an efficient, expressive and repeatable manner. Key features:

  • Automatic garbage collection
  • Extendable frontend formats
  • Concurrent dependency resolution
  • Efficient instruction caching
  • Nested build job invocations
  • Distributable workers
  • Multiple output formats
  • Pluggable architecture
  • Execution without root privileges

I recommend you read this article which will give you general overview about Build Kit. But, to sum up, this is a software whch speed up bulding Docker images. We can enable Build Kit on Azure DevOps by setting DOCKER_BUILDKIT in the pipeline.

variables:
  imageName: 'SampleAppForDocker'
  DOCKER_BUILDKIT: 1

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: Docker@2
  displayName: Login to ACR
  inputs:
    command: login
    containerRegistry: devopsmanual-acr

- task: Docker@2
  displayName: Build and Push
  inputs:
    repository: $(imageName)
    command: buildAndPush
    Dockerfile: build-docker-image/SampleAppForDocker/DOCKERFILE
    tags: |
      build-with-build-kit

- task: Docker@2
  displayName: Logout of ACR
  inputs:
    command: logout
    containerRegistry: devopsmanual-acr

You may wonder how such simple change impacts on build performance ;) It took only 45 seconds.

Summary

In this post, I presented two ways of building docker images. I will not point which is better. It all depends on your preferences. Besides, ACR tasks have more capabilities than I presented here, but this is for the next post.