A Mutable Log

Build .NET containers using multistage Dockerfile

All that you need to build Docker containers in your chosen technology stack is a Linux machine running Docker, and a build automation system such as GitLab or Jenkins that builds from source code and pushes containers out to a container registry.

This post shows how to build containers for .NET Core using a multistage Docker build. Multistage builds allow you to build an image based on only the output you need from prior stages.

Initialize an empty git repo and create a folder called src. Use the dotnet command to create a console or web application in the src folder.

If you’ll run container builds on the development machine, add a .dockerignore file in the root directory so that certain directories don’t get copied over into the image being built

**/bin
**/obj
**/out
**/.vscode
**/.vs
.dotnet
**/node_modules

We’ll build a Dockerfile in stages.

First, we’ll restore dotnet packages, after copying just the .NET solution and project files to the image

FROM mcr.microsoft.com/dotnet/core/sdk:2.2-alpine AS dotnet-package-restore
COPY ./src/MyService.sln /src/MyService.sln
COPY ./src/MyService/MyService.csproj /src/MyService/MyService.csproj
COPY ./src/MyServiceTests/MyServiceTests.csproj /src/MyServiceTests/MyServiceTests.csproj
RUN dotnet restore /src/MyService.sln

Docker caches each build stage by default, so future builds are faster if there are no changes in the files.

Next, if you’re building a web app—we’re building an Angular SPA, for instance—you may need a stage that installs packages using npm

FROM node:lts-alpine AS nodejs-package-install
ARG NPM_TOKEN
COPY ./src/MyService/ClientApp/.npmrc /src/MyService/ClientApp/.npmrc
COPY ./src/MyService/ClientApp/package.json /src/MyService/ClientApp/package.json
COPY ./src/MyService/ClientApp/package-lock.json /src/MyService/ClientApp/package-lock.json
WORKDIR /src/MyService/ClientApp
RUN npm install

.npmrc receives private module credentials through the environment variable NPM_TOKEN.

Next, build the web app, after copying the /src folder from the previous stage, and all the source code

FROM nodejs-package-install AS nodejs-builder
ARG NPM_TOKEN
COPY --from=dotnet-package-restore /src /src
COPY ./src/MyService/ClientApp /src/MyService/ClientApp
WORKDIR /src/MyService/ClientApp
RUN npm run build -- --prod
RUN rm -f /src/MyService/ClientApp/.npmrc

Next, you can use the output of the build stage to run unit tests using npm

FROM nodejs-builder AS nodejs-test
RUN npm test

Next, we’ll copy the /src folder from the nodejs-builder stage, and use the .NET SDK to build and publish all runtime dependencies

FROM dotnet-package-restore AS dotnet-builder
COPY ./src /src
COPY --from=nodejs-builder /src /src
RUN dotnet publish -c Release /src/MyService.sln

If dotnet-builder is your first stage—when you are building a console application, for instance—you can eliminate the COPY statement above.

Next, we’ll run unit tests and check code coverage using coverlet

FROM dotnet-builder AS dotnet-test
ENV ASPNETCORE_ENVIRONMENT=Development
RUN dotnet test /p:CollectCoverage=true /p:Threshold=80 /src/MyServiceTests/MyServiceTests.csproj

The last stage builds the image that will get released out to the container repository

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-alpine AS release
RUN mkdir /service
COPY --from=dotnet-builder /src/MyService/bin/Release/netcoreapp2.2/publish /service
WORKDIR /service
CMD [ "dotnet", "MyService.dll" ]

If you need just the basic .NET runtime, change base image mcr.microsoft.com/dotnet/core/aspnet:2.2-alpine to mcr.microsoft.com/dotnet/core/runtime:2.2-alpine.