4 minute read

There are 52 Dockerfiles in the source code for my book, Docker on Windows. Perfect for a year-long blog series.

Each week I’ll look at one Dockerfile in detail, showing you what it does and how it works. This is #6 in the series.

ch02-dotnet-helloworld:multistage

It’s the same .NET Core “Hello World” console app from #4 and #5 but now using Docker multi-stage builds.

Multi-stage builds use a Dockerfile with multiple FROM instructions. Each FROM starts a new stage in the build process. Intermediate stages are only used during the build, and it’s the final stage that packages the application image.

Multi-stage builds make your app truly portable. You compile the app in one stage of the build, and package the compiled output in the final stage.

With Docker multi-stage builds, anyone can build and run your app from source with just one dependency: Docker.

The compile stage of the build uses a Docker image which has the toolchain installed. The app is compiled in a container, so developers use the exact same build tools as the CI process. The CI servers don’t need any tools installed except Docker.

This example uses .NET Core, but you can use multi-stage builds for any runtime - like .NET Framework apps running in Windows containers and Node.js and Java apps running in Linux containers.

The Dockerfile

Dockerfile.multistage is very simple:

FROM microsoft/dotnet:1.1-sdk-nanoserver AS builder

WORKDIR /src
COPY src/ .

RUN dotnet restore; dotnet publish

# final image stage
FROM microsoft/dotnet:1.1-runtime-nanoserver

WORKDIR /dotnetapp
COPY --from=builder /src/bin/Debug/netcoreapp1.1/publish .

CMD ["dotnet", "HelloWorld.NetCore.dll"]

Stage 1 is the build process. It starts from the microsoft/dotnet image, using the variant built on Nano Server, with the .NET Core 1.1 SDK installed:

FROM... AS builder - the normal FROM instruction, specifying the base image to use, but the AS parameter lets you label the stage so you can refer to it later in the Dockerfile

WORKDIR and COPY just sets up the target directory and copies in the source code from the host

RUN compiles the app using dotnet restore and dotnet publish.

It’s better to break out the restore and build steps, to make use of Docker’s image cache and speed up your build process - but I’ll cover that later in the series.

At the end of this stage, the published app binaries are available in the builder at a known location.

Stage 2 packages the app. It uses the slimmed-down microsoft/dotnet image with the 1.1 runtime installed, but not the SDK. There’s nothing new here except:

  • COPY --from=builder... copies the published app output into the final image. It’s the normal COPY instruction syntax, but the from parameter specifies a previous stage in the build to use as the file source, rather than the host running the build.

Usage

Like the original example you don’t need .NET Core installed to build and run this app. The toolchain is all in the SDK image used in the first stage of the build, so you can just clone the sixeyed/docker-on-windows repo, and run:

cd .\ch02\ch02-dotnet-helloworld

docker image build `
 --tag docker-on-windows/ch02-dotnet-hello-world:multistage `
 --file .\Dockerfile.multistage .

You’ll see all the output from NuGet and MSBuild running inside the build container:

Building .NET Core app in Docker

Now the app is packaged in a Docker image, but like the slim example, the app image uses a slimmer base image, with just the bare minimum needed to run the published app.

The output is a Docker image like any other, the build stages are discarded and they’re not part of the final image. You can push and pull the image to Docker registries and run containers from it in the usual way:

docker container run `
 docker-on-windows/ch02-dotnet-hello-world:multistage

Running .NET Core app in Docker

Going Multi-stage

Multi-stage builds bring a whole lot of benefits to your project:

the entire build and deployment process is encapsulated in the Dockerfile, there’s no proliferation of build scripts to navigate

the toolchain is fixed in the Dockerfile, so it’s the same for everyone. You won’t get into the situation where devs and CI agents have drifting toolchains and builds which work in dev fail on the server

your on-boarding requirements are minimal. New devs on the team and new CI servers only need two things: the source tree and Docker. You won’t lose half a day installing tools just to run a new project

you don’t need Visual Studio installed on the servers, so you can run headless build agents based on Windows Server Core. That means less patching and more automation

devs can even use different IDEs if they want to. They can code away in Visual Studio, Rider or VS Code. They’ll all use the same build process when they run the app locally in a container.

You can do interesting things with multi-stage builds, it’s not just about compiling the app. If you have specific dependencies for your build process or your app, you can use multiple stages with other Docker images as the source for dependencies.

Later in the book I package Jenkins to run in a Docker container on Windows. The Dockerfile starts like this:

FROM dockeronwindows/ch10-git AS git
FROM dockeronwindows/ch10-docker AS docker
FROM dockeronwindows/ch10-jenkins-base

Next Up

Chapter 2 explores the Dockerfile syntax and the image building process. Next week it’s dockeronwindows/ch02-static-website, a simple website which demonstrates how Docker uses temporary containers during the build.

Comments