Containers, containers, containers!

Reducing the size of Docker images…

…by separating build from release (pretty much).

In this article I’ll show you how to the following:

  • Build a Hello World Console Application using .NET Core
  • Use Docker to build the app
  • Use Docker to create a production image of the app

As a prerequisite, you should install .NET core and Docker (either for Mac or Windows).


Hello Console — create, build and run
We are going to use .NET Core CLI to create a new .NET Core project. Start by creating an empty folder, because the CLI will just blindly drop the files into the current folder.

From an empty folder, you can run dotnet new console to create a new Console Application. This command creates a project file (named by the folder command was executed in) and a Program.cs file.

Next, you need to run dotnet restore to pull down the NuGet packages, followed by dotnet build to actually build the project. Build command creates the binaries in /bin/Debug/netcoreapp1.1 folder.

Finally, to run the project, do dotnet run to get the “Hello World!” output. Alternatively, you can also run dotnet /bin/Debug/netcoreapp1.1/[project_name].dll

Last command to know at this point is the publish command that can be used to create release binaries for your project. Here’s an example on how to run it:dotnet publish -c Release -o outputFolder

First argument is the configuration to use (i.e. Release or Debug) and the second out is the output folder where the binaries should be dropped to.


Build the project using Docker
Generic steps to create release binaries for our project are as follows:

  1. Restore the NuGet packages (dotnet restore)
  2. Build the project (dotnet build)
  3. Publish the binaries (dotnet publish -c Release -o outputFolder)

If we take the above steps and translate them to Docker file, we get this:

FROM microsoft/dotnet:1.1-sdk
COPY *.csproj /app/helloworld/
WORKDIR /app/helloworld
RUN ["dotnet", "restore"]
COPY . /app/helloworld
RUN ["dotnet", "build"]
CMD ["dotnet", "publish", "-c", "Release", "-o", "/app/release"]

We will call this file Dockerfile.build and use it to build the release binaries.

Let’s build the image first:

docker build -t helloworld-build -f Dockerfile.build .

and then run it to publish the binaries to the /release folder:

docker run -v $(PWD)/release:/app/release/ helloworld-build

The first command above creates an image that’s about 880 MB in size. This is way to large and we will reduce it by using a runtime image and copying the built binaries (created by the second command that executes the CMD from the Dockerfile.build).


Create a production image
Now that we have the release binaries we can create a production Dockerfile that uses a .NET core runtime image and we copy the built binaries into it.

Let’s create a file named Dockerfile.prod with the following contents:

FROM microsoft/dotnet:1.1-runtime
WORKDIR /app/helloworld
ENTRYPOINT ["dotnet", "helloworld.dll"]
COPY release /app/helloworld

To quickly explain what this does: it copies the release folder from our client where the binaries have been dropped to the /app/helloworld folder inside of the container, sets the working directory and simply runs dotnet helloworld.dll to run the project.

Build the production image with this command:

docker build -t helloworld-prod -f Dockerfile.prod .

and run it like this:

docker run helloworld-prod

If everything worked fine, you should see the Hello World! output. Finally, let’s run docker images to see what the size of the production image is:

$ docker images --format "{{.Repository}} - {{.Size}}"
helloworld-prod - 252 MB
helloworld-build - 884 MB

Wow — down to 252MB! That’s a substantial reduction. Note that this pattern works for other (compiled) languages as well.

Show your support

Clapping shows how much you appreciated Peter Jausovec’s story.