In a previous post, I showed how you can create NuGet packages when you build your app in Docker using the .NET Core CLI. As part of that, I showed how to set the version number for the package using MSBuild commandline switches.
That works well when you're directly calling dotnet build
and dotnet pack
yourself, but what if you want to perform those tasks in a "builder" Dockerfile, like I showed previously. In those cases you need to use a slightly different approach, which I'll describe in this post.
I'll start with a quick recap on using an ONBUILD
builder, and how to set the version number of an app, and then I'll show the solution for how to combine the two. In particular, I'll show how to create a builder and a "downstream" app's Dockerfile where
- Calling
docker build
with--build-arg Version=0.1.0
on your app's Dockerfile, will set the version number for your app in the builder image - You can provide a default version number in your app's Dockerfile, which is used if you don't provide a
--build-arg
- If the downstream image does not set the version, the builder Dockerfile uses a default version number.
Previous posts in this series:
- Exploring the .NET Core Docker files: dotnet vs aspnetcore vs aspnetcore-build
- Building ASP.NET Core apps using Cake in Docker
- Optimising ASP.NET Core apps in Docker - avoiding manually copying csproj files
- Optimising ASP.NET Core apps in Docker - avoiding manually copying csproj files (Part 2)
- Creating a generalised Docker image for building ASP.NET Core apps using ONBUILD
- Creating NuGet packages in Docker using the .NET Core CLI
Using ONBUILD to create builder images
The ONBUILD
command allows you to specify a command that should be run when a "downstream" image is built. This can be used to create "builder" images that specify all the steps to build an application or library, reducing the boilerplate in your application's Dockerfile.
For example, in a previous post I showed how you could use ONBUILD
to create a generic ASP.NET Core builder Dockerfile, reproduced below:
# Build image
FROM microsoft/aspnetcore-build:2.0.7-2.1.105 AS builder
WORKDIR /sln
ONBUILD COPY ./*.sln ./NuGet.config ./
# Copy the main source project files
ONBUILD COPY src/*/*.csproj ./
ONBUILD RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
# Copy the test project files
ONBUILD COPY test/*/*.csproj ./
ONBUILD RUN for file in $(ls *.csproj); do mkdir -p test/${file%.*}/ && mv $file test/${file%.*}/; done
ONBUILD RUN dotnet restore
ONBUILD COPY ./test ./test
ONBUILD COPY ./src ./src
ONBUILD RUN dotnet build -c Release --no-restore
ONBUILD RUN find ./test -name '*.csproj' -print0 | xargs -L1 -0 dotnet test -c Release --no-build --no-restore
By basing your app Dockerfile on this image (in the FROM
statement), your application would be automatically restored, built and tested, without you having to include those steps yourself. Instead, your app image could be very simple, for example:
# Build image
FROM andrewlock/aspnetcore-build:2.0.7-2.1.105 as builder
# Publish
RUN dotnet publish "./AspNetCoreInDocker.Web/AspNetCoreInDocker.Web.csproj" -c Release -o "../dist" --no-restore
#App image
FROM microsoft/aspnetcore:2.0.7
WORKDIR /app
ENV ASPNETCORE_ENVIRONMENT Local
ENTRYPOINT ["dotnet", "AspNetCoreInDocker.Web.dll"]
COPY /sln/dist .
Setting the version number when building your application
You often want to set the version number of a library or application when you build it - you might want to record the app version in log files when it runs for example. Also, when building NuGet packages you need to be able to set the package version number. There are a variety of different version numbers available to you (as I discussed in a previous post/), all of which can be set from the command line when building your application.
In my last post I described how to set version numbers using MSBuild switches. For example, to set the Version
MSBuild property when building (which, when set, updates all the other version numbers of the assembly) you could use the following command
dotnet build /p:Version=0.1.2-beta -c Release --no-restore
Setting the version in this way is the same whether you're running it from the command line, or in Docker. However, in your Dockerfile, you will typically want to pass the version to set as a build argument. For example, the following command:
docker build --build-arg Version="0.1.0" .
could be used to set the Version
property to 0.1.0
by using the ARG
command, as shown in the following Dockerfile:
FROM microsoft/dotnet:2.0.3-sdk AS builder
ARG Version
WORKDIR /sln
COPY . .
RUN dotnet restore
RUN dotnet build /p:Version=$Version -c Release --no-restore
RUN dotnet pack /p:Version=$Version -c Release --no-restore --no-build
Using ARGs in a parent Docker image that uses ONBUILD
The two techniques described so far work well in isolation, but getting them to play nicely together requires a little bit more work. The initial problem is to do with the way Docker treats builder images that use ONBUILD
.
To explore this, imagine you have the following, simple, builder image, tagged as andrewlock/testbuild
:
FROM microsoft/aspnetcore-build:2.0.7-2.1.105 AS builder
WORKDIR /sln
ONBUILD COPY ./test ./test
ONBUILD COPY ./src ./src
ONBUILD RUN dotnet build -c Release
Warning: This Dockerfile has no optimisations, don't use it for production!
As a first attempt, you might try just adding the ARG
command to your downstream image, and passing the --build-arg
in. The following is a very simple Dockerfile that uses the builder, and accepts an argument.
# Build image
FROM andrewlock/testbuild as builder
ARG Version
# Publish
RUN dotnet publish "./AspNetCoreInDocker.Web/AspNetCoreInDocker.Web.csproj" -c Release -o --no-restore
Calling docker build --build-arg Version="0.1.0" .
will build the image, and set the $Version
parameter in the downstream dockerfile to 0.1.0
, but that won't be used in the builder Dockerfile at all, so it would only be useful if you're running dotnet pack
in your downstream image for example.
Instead, you can use a couple of different characteristics about Dockerfiles to pass values up from your downstream app's Dockerfile to the builder Dockerfile.
- Any
ARG
defined before the firstFROM
is "global", so it's not tied to a builder stage. Any stage that wants to use it, still needs to declare its ownARG
command - You can provide default values to
ARG
commands using the formatARG value=default
- You can combine
ONBUILD
withARG
Lets combine all these features, and create our new builder image.
A builder image that supports setting the version number
I've cut to the chase a bit here - needless to say I spent a while fumbling around, trying to get the Dockerfiles doing what I wanted. The solution shown in this post is based on the excellent description in this issue.
The annotated builder image is as follows. I've included comments in the file itself, rather than breaking it down afterwards. As before, this is a basic builder image, just to demonstrate the concept. For a Dockerfile with all the optimisations see my builder image on Dockerhub.
FROM microsoft/aspnetcore-build:2.0.7-2.1.105 AS builder
# This defines the `ARG` inside the build-stage (it will be executed after `FROM`
# in the child image, so it's a new build-stage). Don't set a default value so that
# the value is set to what's currently set for `BUILD_VERSION`
ONBUILD ARG BUILD_VERSION
# If BUILD_VERSION is set/non-empty, use it, otherwise use a default value
ONBUILD ARG VERSION=${BUILD_VERSION:-1.0.0}
WORKDIR /sln
ONBUILD COPY ./test ./test
ONBUILD COPY ./src ./src
ONBUILD RUN dotnet build -c Release /p:Version=$VERSION
I've actually defined two arguments here, BUILD_VERSION
and VERSION
. We do this to ensure that we can set a default version in the builder image, while also allowing you to override it from the downstream image or by using --build-arg
.
Those two additional ONBUILD ARG
lines are all you need in your builder Dockerfile. You need to either update your downstream app's Dockerfile as shown below, or use --build-arg
to set the BUILD_VERSION
argument for the builder to use.
If you want to set the version number with --build-arg
If you just want to provide the version number as a --build-arg
value, then you don't need to change your downstream image. You could use the following:
FROM andrewlock/testbuild as builder
RUN dotnet publish "./AspNetCoreInDocker.Web/AspNetCoreInDocker.Web.csproj" -c Release -o --no-restore
And then set the version number when you build:
docker build --build-arg BUILD_VERSION="0.3.4-beta" .
That would pass the BUILD_VERSION
value up to the builder image, which would in turn pass it to the dotnet build
command, setting the Version
property to 0.3.4-beta
.
If you don't provide the --build-arg
argument, the builder image will use its default value (1.0.0
) as the build number.
Note that this will overwrite any version number you've set in your csproj files, so this approach is only any good for you if you're relying on a CI process to set your version numbers
If you want to set a default version number in your downstream Dockerfile
If you want to have the version number of your app checked in to source, then you can set a version number in your downstream Dockerfile. Set the BUILD_VERSION
argument before the first FROM
command in your app's Dockerfile:
ARG BUILD_VERSION=0.2.3
FROM andrewlock/testbuild as builder
RUN dotnet publish "./AspNetCoreInDocker.Web/AspNetCoreInDocker.Web.csproj" -c Release -o --no-restore
Running docker build .
on this file will ensure that the libraries built in the builder file have a version of 0.2.3
.
If you wish to overwrite this at runtime you can simply pass in the build argument as before:
docker build --build-arg BUILD_VERSION="0.3.4-beta" .
And there you have it! ONBUILD
playing nicely with ARG
. If you decide to adopt this pattern in your builder images, just be aware that you will no longer be able to change the version number by setting it in your csproj files.
Summary
In this post I described how you can use ONBUILD
and ARG
to dynamically set version numbers for your .NET libraries when you're using a generalised builder image. For an alternative description (and the source of this solution), see this issue on GitHub and the provided examples.