Problem: When running tasks like dotnet restore inside a Docker multi-stage build, it will fail if nuget.config refers to a Azure Artifacts feed due to 401 - Unauthorized.

If using the built-in Azure DevOps tasks for restoring packages, this is taken care for you - once you do this inside a container, you are on your own.

Our base Dockerfile

FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
WORKDIR /app

FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY nuget.config .
COPY *.csproj .
RUN dotnet restore
COPY . .
WORKDIR /src
RUN dotnet build -c Release -o /app MyApp.csproj

FROM build AS publish
RUN dotnet publish -c Release -o /app MyApp.csproj

FROM base AS final
ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "MyApp.dll"]

Requirements

We have a few requirements when we do automated builds:

  • Security: No tokens should be part of scripts, Dockerfiles, etc
  • Personal Access Tokens: Key part here is personal - we don't want build pipelines to be dependent on a user - what if the user quits the organization or clears out their tokens?

We also want to use multi-stage Dockerfiles so that we have the same build setup locally as our CI/CD system.

Setting up our Dockerfile

In order to authenticate to Azure Artifacts, we need to install a credential provider for NuGet.

So lets add the following to our build image:

RUN curl -L 'https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh' | bash

We then need to pass in a token for the credential provider to use since we cannot rely on a interactive login.

The credential provider looks for the presence of a environment variables named VSS_NUGET_EXTERNAL_FEED_ENDPOINTS and its value should be a json containing the following:

{
"endpointCredentials": [
    {
        "endpoint":"https://our Azure Artifacts endpoint/", 
        "username":"optional", 
        "password":"secret token"
    }
]}

We also don't want to have things like the token hardcoded and checked into source control, so we pass it in using Docker build args.

ARG ACCESS_TOKEN
ARG ARTIFACTS_ENDPOINT
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS "{\"endpointCredentials\": [{\"endpoint\":\"${ARTIFACTS_ENDPOINT}\", \"password\":\"${ACCESS_TOKEN}\"}]}"

These arguments only have value while building the image and won't persist.

Our resulting Dockerfile should look something like this:

FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
WORKDIR /app

FROM microsoft/dotnet:2.2-sdk AS build
RUN curl -L 'https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh' | bash
ARG ACCESS_TOKEN
ARG ARTIFACTS_ENDPOINT
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS "{\"endpointCredentials\": [{\"endpoint\":\"${ARTIFACTS_ENDPOINT}\", \"password\":\"${ACCESS_TOKEN}\"}]}"
WORKDIR /src
COPY nuget.config .
COPY *.csproj .
RUN dotnet restore
COPY . .
WORKDIR /src
RUN dotnet build -c Release -o /app MyApp.csproj

FROM build AS publish
RUN dotnet publish -c Release -o /app MyApp.csproj

FROM base AS final
ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "MyApp.dll"]

Creating a Azure DevOps pipeline

Our base pipeline configuration could look like this:


trigger:
- master

pool:
  vmImage: 'Ubuntu-16.04'

variables:

steps:

First, we need to have some variables set up in Azure DevOps.

The best way is to create a linked group so that these variables can be reused easily between pipelines.

Go to your project at Azure DevOps and click Library under Pipelines

Add your Azure Artifacts feed URI as the value here - the variable name can be whatever you like, just remember it for when we make use of it in the pipeline configuration.

Let's reference this linked group in our config by modifying variables

variables:
- group: Common

Next we need to add the actual steps for building the Docker image.

Here we will reference our variable from above (ArtifactFeed) and one key piece: System.AccessToken - this is a special variable that carries the security token used by the running build and which is our way in for the Azure Artifacts authentication.

steps:
- task: Docker@1
  displayName: 'Build using multi-stage'
  inputs:
    containerregistrytype: 'Container Registry'
    dockerRegistryEndpoint: 'My Container Registry'
    arguments: '--build-arg ACCESS_TOKEN=$(System.AccessToken) --build-arg ARTIFACTS_ENDPOINT=$(ArtifactFeed)'

Final azure-pipelines.yaml can then look like this:

trigger:
  batch: true
  branches:
    include:
    - master
  paths:
    exclude:
    - README.md

pool:
  vmImage: 'Ubuntu-16.04'

variables:
- group: Common

steps:
- task: Docker@1
  displayName: 'Build using multi-stage'
  inputs:
    containerregistrytype: 'Container Registry'
    dockerRegistryEndpoint: 'My Container Registry'
    arguments: '--build-arg ACCESS_TOKEN=$(System.AccessToken) --build-arg ARTIFACTS_ENDPOINT=$(ArtifactFeed)'

- task: Docker@1
  displayName: 'Push to container registry'
  inputs:
    containerregistrytype: 'Container Registry'
    dockerRegistryEndpoint: 'My Container Registry'
    command: 'Push an image'

Create the build pipeline (outside scope of this post) and queue a build!