blog post image
Andrew Lock avatar

Andrew Lock

~16 min read

Exploring the .NET Core Docker files: dotnet vs aspnetcore vs aspnetcore-build

Note: Things have changed a lot in .NET Core 2.1, so this post is out of date. See the updated post for details.

When you build and deploy an application in Docker, you define how your image should be built using a Dockerfile. This file lists the steps required to create the image, for example: set an environment variable, copy a file, or run a script. Whenever a step is run, a new layer is created. Your final Docker image consists of all the changes introduced by these layers in your Dockerfile.

Typically, you don't start from an empty image where you need to install an operating system, but from a "base" image that contains an already configured OS. For .NET development, Microsoft provide a number of different images depending on what it is you're trying to achieve.

In this post, I look at the various Docker base images available for .NET Core development, how they differ, and when you should use each of them. I'm only going to look at the Linux amd64 images, but there are Windows container versions and even Linux arm32 images available too. At the time of writing the latest images available are 2.1.2 and 2.0.3 for the sdk-based and runtime-based images respectively.

Note: You should normally be specific about exactly which version of a Docker image you build on in your Dockerfiles (e.g. don't use latest). For that reason, all the image I mention in this post use the current latest version suffix, 2.0.3.

I'll start by briefly discussing the difference between the .NET Core SDK and the .NET Core Runtime, as it's an important factor when deciding which base image you need. I'll then walk through each of the images in turn, using the Dockerfiles for each to explain what they contain, and hence what you should use them for.

tl;dr; This is a pretty long post, so for convenience, here's some links to the relevant sections and a one-liner use case:

The .NET Core Runtime vs the .NET Core SDK

One of the most often lamented aspects of .NET Core and .NET Core development, is around version numbers. There are so many different moving parts, and none of the version numbers match up, so it can be difficult to figure out what you need.

For example, on my dev machine I am building .NET Core 2.0 apps, so I installed the .NET Core 2.x SDK to allow me to do so. When I look at what I have installed using dotnet --info, I get the following:

> dotnet --info
.NET Command Line Tools (2.1.2)

Product Information:
 Version:            2.1.2
 Commit SHA-1 hash:  5695315371

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.16299
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\2.1.2\

Microsoft .NET Core Shared Framework Host

  Version  : 2.0.3
  Build    : a9190d4a75f4a982ae4b4fa8d1a24526566c69df

There's a lot of numbers there, but the important ones are 2.1.2 which is the version of the command line tools or SDK I have installed, and 2.0.3 which is the version of the .NET Core runtime I have installed.

I genuinely have no idea why the SDK is version 2.1.2 - I thought it was 2.0.3 as well but apparently not. This is made all the more confusing by the fact the 2.1.2 version isn't mentioned anywhere in any of the Docker images. Welcome to the brave new world.

Whether you need the .NET Core SDK or the .NET Core runtime depends on what you're trying to do:

  • The .NET Core SDK - This is what you need to build .NET Core applications.
  • The .NET Core Runtime - This is what you need to run .NET Core applications.

When you install the SDK, you get the runtime as well, so on your dev machines you can just install the SDK. However, when it comes to deployment you need to give it a little more thought. The SDK contains everything you need to build a .NET Core app, so it's much larger than the runtime alone (122MB vs 22MB for the MSI files). If you're just going to be running the app on a machine (or in a Docker container) then you don't need the full SDK, the runtime will suffice, and will keep the image as small as possible.

For the rest of this post, I'll walk through the main Docker images available for .NET Core and ASP.NET Core. I assume you have a working knowledge of Docker - if you're new to Docker I suggest checking out Steve Gordon's excellent series on Docker for .NET developers.

1. microsoft/dotnet:2.0.3-runtime-deps

  • Contains native dependencies
  • No .NET Core runtime or .NET Core SDK installed
  • Use for running Self-Contained Deployment apps

The first image we'll look at forms the basis for most of the other .NET Core images. It actually doesn't even have .NET Core installed. Instead, it consists of the base debian:stretch image and has all the low-level native dependencies on which .NET Core depends.

The Dockerfile consists of a single RUN command that apt-get installs the required dependencies on top of the base image.

FROM debian:stretch

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
        \
# .NET Core dependencies
        libc6 \
        libcurl3 \
        libgcc1 \
        libgssapi-krb5-2 \
        libicu57 \
        liblttng-ust0 \
        libssl1.0.2 \
        libstdc++6 \
        libunwind8 \
        libuuid1 \
        zlib1g \
    && rm -rf /var/lib/apt/lists/*

What should you use it for?

The microsoft/dotnet:2.0.3-runtime-deps image is the basis for subsequent .NET Core runtime installations. Its main use is for when you are building self-contained deployments (SCDs). SCDs are app that are packaged with the .NET Core runtime for the specific host, so you don't need to install the .NET Core runtime. You do still need the native dependencies though, so this is the image you need.

Note that you can't build SCDs with this image. For that, you'll need one of the SDK-based images described later in the post, such as microsoft/dotnet:2.0.3-sdk.

2. microsoft/dotnet:2.0.3-runtime

  • Contains .NET Core runtime
  • Use for running .NET Core console apps

The next image is one you'll use a lot if you're running .NET Core console apps in production. microsoft/dotnet:2.0.3-runtime builds on the runtime-deps image, and installs the .NET Core Runtime. It downloads the tar ball using curl, verifies the hash, unpacks it, sets up symlinks and removes the old installer.

You can view the Dockerfile for the image here:

FROM microsoft/dotnet:2.0-runtime-deps

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

# Install .NET Core
ENV DOTNET_VERSION 2.0.3
ENV DOTNET_DOWNLOAD_URL https://dotnetcli.blob.core.windows.net/dotnet/Runtime/$DOTNET_VERSION/dotnet-runtime-$DOTNET_VERSION-linux-x64.tar.gz
ENV DOTNET_DOWNLOAD_SHA 4FB483CAE0C6147FBF13C278FE7FC23923B99CD84CF6E5F96F5C8E1971A733AB968B46B00D152F4C14521561387DD28E6E64D07CB7365D43A17430905DA6C1C0

RUN curl -SL $DOTNET_DOWNLOAD_URL --output dotnet.tar.gz \
    && echo "$DOTNET_DOWNLOAD_SHA dotnet.tar.gz" | sha512sum -c - \
    && mkdir -p /usr/share/dotnet \
    && tar -zxf dotnet.tar.gz -C /usr/share/dotnet \
    && rm dotnet.tar.gz \
    && ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet

What should you use it for?

The microsoft/dotnet:2.0.3-runtime image contains the .NET Core runtime, so you can use it to run any .NET Core 2.0 app such as a console app. You can't use this image to build your app, only to run it.

If you're running a self-contained app then you would be better served by the runtime-deps image. Similarly, if you're running an ASP.NET Core app, then you should use the microsoft/aspnetcore:2.0.3 image instead (up next), as it contains optimisations for running ASP.NET Core apps.

3. microsoft/aspnetcore:2.0.3

  • Contains .NET Core runtime and the ASP.NET Core runtime store
  • Use for running ASP.NET Core apps
  • Sets the default URL for apps to http://+:80

.NET Core 2.0 introduced a new feature called the runtime store. This is conceptually similar to the Global Assembly Cache (GAC) from .NET Framework days, though without some of the issues.

Effectively, you can install certain NuGet packages globally by adding them to a Runtime Store. ASP.NET Core does this by registering all of the Microsoft NuGet packages that make up the Microsoft.AspNetCore.All metapackage with the runtime store (as described in this post/). When your app is published, it doesn't need to include any of the dlls that are in the store. This makes your published output smaller, and improves layer caching for Docker images.

The microsoft/aspnetcore:2.0.3 image builds on the previous .NET Core runtime image, and simply installs the ASP.NET Core runtime store. It also sets the default listening URL for apps to port 80 by setting the ASPNETCORE_URLS environment variable.

You can view the Dockerfile for the image here:

FROM microsoft/dotnet:2.0.3-runtime-stretch

# set up network
ENV ASPNETCORE_URLS http://+:80
ENV ASPNETCORE_PKG_VERSION 2.0.3

# set up the runtime store
RUN for version in '2.0.0' '2.0.3'; do \
        curl -o /tmp/runtimestore.tar.gz https://dist.asp.net/runtimestore/$version/linux-x64/aspnetcore.runtimestore.tar.gz \
        && export DOTNET_HOME=$(dirname $(readlink $(which dotnet))) \
        && tar -x -C $DOTNET_HOME -f /tmp/runtimestore.tar.gz \
        && rm /tmp/runtimestore.tar.gz; \
    done

What should you use it for?

Fairly obviously, for running ASP.NET Core apps! This is the image to use if you've published an ASP.NET Core app and you need to run it in production. It has the smallest possible footprint (ignoring the Alpine-based images for now!) but all the necessary framework components and optimisations. You can't use it for building your app though, as it doesn't have the SDK installed. For that, you need one of the upcoming images.

4. microsoft/dotnet:2.0.3-sdk

  • Contains .NET Core SDK
  • Use for building .NET Core apps
  • Can also be used for building ASP.NET Core apps

We're onto the first of the .NET Core SDK images now. These images can all be used for building your apps. Unlike all the runtime images which use debian:stretch as the base, the microsoft/dotnet:2.0.3-sdk image (and those that build on it) use the buildpack-deps:stretch-scm image. According to the Docker Hub description, the buildpack image:

…includes a large number of "development header" packages needed by various things like Ruby Gems, PyPI modules, etc.…a majority of arbitrary gem install / npm install / pip install should be successful without additional header/development packages…

The stretch-scm tag also ensures common tools like curl, git, and ca-certificates are installed.

The microsoft/dotnet:2.0.3-sdk image installs the native prerequisites (as you saw in the microsoft/dotnet:2.0.3-runtime-deps image), and then installs the .NET Core SDK. Finally, it warms up the NuGet cache by running dotnet new in an empty folder, which makes subsequent dotnet operations in derived images faster.

You can view the Dockerfile for the image here:

FROM buildpack-deps:stretch-scm

# Install .NET CLI dependencies
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        libc6 \
        libcurl3 \
        libgcc1 \
        libgssapi-krb5-2 \
        libicu57 \
        liblttng-ust0 \
        libssl1.0.2 \
        libstdc++6 \
        libunwind8 \
        libuuid1 \
        zlib1g \
    && rm -rf /var/lib/apt/lists/*

# Install .NET Core SDK
ENV DOTNET_SDK_VERSION 2.0.3
ENV DOTNET_SDK_DOWNLOAD_URL https://dotnetcli.blob.core.windows.net/dotnet/Sdk/$DOTNET_SDK_VERSION/dotnet-sdk-$DOTNET_SDK_VERSION-linux-x64.tar.gz
ENV DOTNET_SDK_DOWNLOAD_SHA 74A0741D4261D6769F29A5F1BA3E8FF44C79F17BBFED5E240C59C0AA104F92E93F5E76B1A262BDFAB3769F3366E33EA47603D9D725617A75CAD839274EBC5F2B

RUN curl -SL $DOTNET_SDK_DOWNLOAD_URL --output dotnet.tar.gz \
    && echo "$DOTNET_SDK_DOWNLOAD_SHA dotnet.tar.gz" | sha512sum -c - \
    && mkdir -p /usr/share/dotnet \
    && tar -zxf dotnet.tar.gz -C /usr/share/dotnet \
    && rm dotnet.tar.gz \
    && ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet

# Trigger the population of the local package cache
ENV NUGET_XMLDOC_MODE skip
RUN mkdir warmup \
    && cd warmup \
    && dotnet new \
    && cd .. \
    && rm -rf warmup \
    && rm -rf /tmp/NuGetScratch

What should you use it for?

This image has the .NET Core SDK installed, so you can use it for building your .NET Core apps. You can build .NET Core console apps or ASP.NET Core apps, though in the latter case you may prefer one of the alternative images coming up in this post.

Technically you can also use this image for running your apps in production as the SDK includes the runtime, but you shouldn't do that in practice. As discussed at the beginning of this post, optimising your Docker images in production is important for performance reasons, but the microsoft/dotnet:2.0.3-sdk image weighs in at a hefty 1.68GB, compared to the 219MB for the microsoft/dotnet:2.0.3-runtime image.

To get the best of both worlds, you should use this image (or one of the later images) to build your app, and one of the runtime images to run your app in production. You can see how to do this using Docker multi-stage builds in Scott Hanselman's post here.

5. microsoft/aspnetcore-build:2.0.3

  • Contains .NET Core SDK
  • Has warmed-up package cache for Microsoft.AspNetCore.All package
  • Installs Node, Bower and Gulp
  • Use for building ASP.NET Core apps

You can happily build ASP.NET Core apps using the microsoft/dotnet:2.0.3-sdk package, but the microsoft/aspnetcore-build:2.0.3 image that builds on it includes a number of additional layers that are often required.

First, it installs Node, Bower, and Gulp into the image. These tools are (were?) commonly used for building client-side apps, so this image makes them available globally.

Finally, the image warms up the package cache for all the common ASP.NET Core packages found in the Microsoft.AspNetCore.All metapackage, so that dotnet restore will be faster for apps based on this image. It does this by copying a .csproj file into a temporary folder and running dotnet restore. The csproj simply references the metapackage (with a version passed via an Environment Variable in the Dockerfile)

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <RuntimeIdentifiers>debian.8-x64</RuntimeIdentifiers>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="$(ASPNETCORE_PKG_VERSION)" />
  </ItemGroup>

</Project>

You can view the Dockerfile for the image here:

FROM microsoft/dotnet:2.0.3-sdk-stretch

# set up environment
ENV ASPNETCORE_URLS http://+:80
ENV NODE_VERSION 6.11.3
ENV ASPNETCORE_PKG_VERSION 2.0.3

RUN set -x \
    && apt-get update && apt-get install -y gnupg dirmngr --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# Install keys required for node
RUN set -ex \
  && for key in \
    9554F04D7259F04124DE6B476D5A82AC7E37093B \
    94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \
    0034A06D9D9B0064CE8ADF6BF1747F4AD2306D93 \
    FD3A5288F042B6850C66B31F09FE44734EB7990E \
    71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \
    DD8F2338BAE7501E3DD5AC78C273792F7D83545D \
    B9AE9905FFD7803F25714661B63B535A4C206CA9 \
    C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \
  ; do \
    gpg --keyserver pgp.mit.edu --recv-keys "$key" || \
    gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key" || \
    gpg --keyserver keyserver.pgp.com --recv-keys "$key" ; \
  done

# set up node
RUN buildDeps='xz-utils' \
    && set -x \
    && apt-get update && apt-get install -y $buildDeps --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
    && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
    && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
    && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
    && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
    && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
    && apt-get purge -y --auto-remove $buildDeps \
    && ln -s /usr/local/bin/node /usr/local/bin/nodejs \
    # set up bower and gulp
    && npm install -g bower gulp \
    && echo '{ "allow_root": true }' > /root/.bowerrc

# warmup NuGet package cache
COPY packagescache.csproj /tmp/warmup/
RUN dotnet restore /tmp/warmup/packagescache.csproj \
      --source https://api.nuget.org/v3/index.json \
      --verbosity quiet \
    && rm -rf /tmp/warmup/

WORKDIR /

What should you use it for?

This image will likely be the main image you use to build ASP.NET Core apps. It contains the .NET Core SDK, the same as microsoft/dotnet:2.0.3-sdk, but it also includes the additional dependencies that are sometimes required to build traditional apps with ASP.NET Core, such as Bower and Gulp.

Even if you're not using those dependencies, the additional warming of the package cache is a nice optimisation. If you opt to use the microsoft/dotnet:2.0.3-sdk image instead for building your apps, I suggest you warm up the package cache in your own Dockerfile in a similar way.

As before, the SDK image is much larger than the runtime image. You should only use this image for building your apps; use one of the runtime images to deploy your app to production.

6. microsoft/aspnetcore-build:1.0-2.0

  • Contains multiple .NET Core SDKs: 1.0, 1.1, and 2.0
  • Has warmed-up package cache for Microsoft.AspNetCore.All package
  • Installs Node, Bower and Gulp
  • Installs the Docker SDK for building solutions containing a Docker tools project
  • Use for building ASP.NET Core apps or anything really!

The final image is one I wasn't even aware of until I started digging around in the aspnet-docker GitHub repository. It's contained in the (aptly titled) kitchensink folder, and it really does have everything you could need to build your apps!

The microsoft/aspnetcore-build:1.0-2.0 image contains the .NET Core SDK for all current major and minor versions, namely .NET Core 1.0, 1.1, and 2.0. This has the advantage that you should be able to build any of your .NET Core apps, even if they are tied to a specific .NET Core version using a global.json file.

Just as for the microsoft/aspnetcore-build:2.0.3 image, Node, Bower, and Gulp are installed, and the package cache for the Microsoft.AspNetCore.All is warmed up. Additionally, the kitchensink image installs the Microsoft.Docker.SDK SDK that is required when building a project that has Docker tools enabled (through Visual Studio).

You can view the Dockerfile for the image here:

FROM microsoft/dotnet:2.0.3-sdk-stretch

# set up environment
ENV ASPNETCORE_URLS http://+:80
ENV NODE_VERSION 6.11.3
ENV NETCORE_1_0_VERSION 1.0.8
ENV NETCORE_1_1_VERSION 1.1.5
ENV ASPNETCORE_PKG_VERSION 2.0.3

RUN set -x \
    && apt-get update && apt-get install -y gnupg dirmngr --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

RUN set -ex \
  && for key in \
    9554F04D7259F04124DE6B476D5A82AC7E37093B \
    94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \
    0034A06D9D9B0064CE8ADF6BF1747F4AD2306D93 \
    FD3A5288F042B6850C66B31F09FE44734EB7990E \
    71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \
    DD8F2338BAE7501E3DD5AC78C273792F7D83545D \
    B9AE9905FFD7803F25714661B63B535A4C206CA9 \
    C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \
  ; do \
    gpg --keyserver pgp.mit.edu --recv-keys "$key" || \
    gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key" || \
    gpg --keyserver keyserver.pgp.com --recv-keys "$key" ; \
  done

# set up node
RUN buildDeps='xz-utils' \
    && set -x \
    && apt-get update && apt-get install -y $buildDeps --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
    && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
    && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
    && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
    && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
    && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
    && apt-get purge -y --auto-remove $buildDeps \
    && ln -s /usr/local/bin/node /usr/local/bin/nodejs \
    # set up bower and gulp
    && npm install -g bower gulp \
    && echo '{ "allow_root": true }' > /root/.bowerrc

# Install the 1.x runtimes
RUN for url in \
      "https://dotnetcli.blob.core.windows.net/dotnet/Runtime/${NETCORE_1_0_VERSION}/dotnet-debian-x64.${NETCORE_1_0_VERSION}.tar.gz" \
      "https://dotnetcli.blob.core.windows.net/dotnet/Runtime/${NETCORE_1_1_VERSION}/dotnet-debian-x64.${NETCORE_1_1_VERSION}.tar.gz"; \
    do \
      echo "Downloading and installing from $url" \
      && curl -SL $url --output /tmp/dotnet.tar.gz \
      && mkdir -p /usr/share/dotnet \
      && tar -zxf /tmp/dotnet.tar.gz -C /usr/share/dotnet \
      && rm /tmp/dotnet.tar.gz; \
    done

# Add Docker SDK for when building a solution that has the Docker tools project.
RUN curl -H 'Cache-Control: no-cache' -o /tmp/Microsoft.Docker.Sdk.tar.gz https://distaspnet.blob.core.windows.net/sdk/Microsoft.Docker.Sdk.tar.gz \
    && cd /usr/share/dotnet/sdk/${DOTNET_SDK_VERSION}/Sdks \
    && tar xf /tmp/Microsoft.Docker.Sdk.tar.gz \
    && rm /tmp/Microsoft.Docker.Sdk.tar.gz

# copy the ASP.NET packages manifest
COPY packagescache.csproj /tmp/warmup/

# warm up package cache
RUN dotnet restore /tmp/warmup/packagescache.csproj \
      --source https://api.nuget.org/v3/index.json \
      --verbosity quiet \
    && rm -rf /tmp/warmup/

WORKDIR /

What should you use it for?

Use this image to build ASP.NET Core (or .NET Core apps) that require multiple .NET Core runtimes or that contain Docker tools projects.

Alternatively, you could use this image if you just want to have a single base image for building all of your .NET Core apps, regardless of the SDK version (instead of using microsoft/aspnetcore-build:2.0.3 for 2.0 projects and microsoft/aspnetcore-build:1.1.5 for 1.1 projects for example).

Summary

In this post I walked through some of the common Docker images used in .NET Core development. Each of the images have a set of specific use-cases, and it's important you use the right one for your requirements.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?