Runar Ovesen Hjerpbakk

Science-based software development

Running ASP.Net Core web app in a Docker container using a multi-stage Dockerfile

Lately. I’ve written multiple web applications with ASP.Net Core. Together with C# newfound cross-platformness, this stack is a joy to work with.

I user Docker to run the applications, some in Azure, some on on-premise Linux boxes. This has thought me a couple of things. Firstly, the app should be built from within a container using a multi-stage Dockerfile.

If you’ve never used Docker, a container (Docker) image is a way to package an app or service and deploy it in a reliable and reproducible way.

Docker is an open-source project for automating the deployment of applications as portable, self-sufficient containers that can run on the cloud or on-premises.

The main goal of an image is that it makes the environment (dependencies) the same across different deployments. Using Docker, you can guarantee that the exact same code runs both in dev and production. Not just your app, but the entire stack!

Multi-stage Dockerfile

To create your Docker image, you need a Dockerfile.

A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Using docker build users can create an automated build that executes several command-line instructions in succession.

I use a two-stage Dockerfile. The application is built and automated tests are run within a container, and if all tests are green, the app is copied to the container responsible for running the application. The image needed to run your app will be much smaller than the one needed to build it.

Thus your build server or friendly neighbourhood developer only needs the docker tools installed to build, test and run your app!

Multi-stage docker build

Using .Net Core, the Dockerfile can look like this:

FROM microsoft/aspnetcore-build:2 AS builder
WORKDIR /source

COPY ./GrafanaTfsDataSource.sln .
COPY ./GrafanaTfsDataSource/*.csproj ./GrafanaTfsDataSource/
COPY ./Tests ./Tests
RUN dotnet restore

COPY ./GrafanaTfsDataSource ./GrafanaTfsDataSource
RUN dotnet test "./Tests/Tests.csproj" -c Release --no-restore

RUN dotnet publish "./GrafanaTfsDataSource/GrafanaTfsDataSource.csproj" --output "../dist" --configuration Release --no-restore

FROM microsoft/aspnetcore:2
WORKDIR /app
COPY --from=builder /source/dist .
EXPOSE 5000
ENTRYPOINT ["dotnet", "GrafanaTfsDataSource.dll"]

microsoft/aspnetcore-build:2 is the build image containing the latest version of the .Net Core 2 SDK. Then the solution file and project is copied into the container, and the dependencies are restored. In the end, all files are copied and the app is published to the dist folder. Docker is excellent at caching things, so if steps are unchanged since the last run, they are executed instantaneously.

The second stage is simpler and based on the microsoft/aspnetcore:2 image containing the latest .Net Core 2 runtime. Here the app is copied from the dist folder and an ENTRYPOINT is defined just like we’re used to.

To make building even simpler for contributors, I always create a simple build script like this:

#! /bin/bash
docker build -t dips/grafana-tfs-data-source .