As a modern company focused on the cloud, you might think we all love Docker and Containers. But the truth is some (amazing) application developers believe this is unnecessary, as they are used to Zipping their files and deploying them to a PaaS service. They are not containerization people: they are .NET developers. And it seems that Microsoft agrees with them. Microsoft believe that writing Dockerfiles is unnecessary and that publishing a .NET application as a container should be as easy as any other deployment.
In this blog post, I will take a deep dive into containerizing a .NET application without the use of Dockerfiles, and show you how to use the dotnet publish command to publish a .NET application as a container.
The Start: Build Image Tool
This is an open-source tool that was developed in April 2022. It calls itself “A .NET global tool to create container images from .NET projects, because life is too short to write Dockerfiles.”
I will not focus on this, but feel obliged to give a quick demo, as it is quite cool! You can find the tool here. Installing it is as easy as running the following command:
After installing the tool, you can run the following command to build an image:
This generates a temporary Dockerfiles and then builds the image. For my API, the Dockerfiles looks like this:
Current Tool: dotnet Publish
This is a new feature added to the .NET 8.0 SDK (.NET 7.0 as a NuGet package). It allows your .NET publish to output a container image. The official documentation with all the options can be found here.
dotnet worker
As with the demo, I will showcase a worker application along with other types of .NET applications. As you’ll see, creating a container image for any resource is straightforward. Simply add the following line to your csproj file:
The container needs a name. After this, it is as simple as running the following command:
Then run the following command:
Demo
This quick demo (code can be found here) uses the battleship game from this open source project. If you look at the csproj file you will see the following lines:
In the terminal you can see the following output:
If I then run it in docker desktop, I see a working battleship game.
API
Creating a container image for an .NET API is extremely easy. You just need to add the following lines to the csproj file:
After this, it is as simple as running the following command:
Azure Function
Creating a container image for an Azure Function is also very easy. You just need to add the following lines to the csproj file:
Then you run:
You will discover then that the Azure Function is not working. This feature is unfortunately not yet supported by the Azure Functions runtime. You can only publish a function as a container if you have a Dockerfile. You can follow the official issue here and here for updates. I really hope they add this soon, as Azure Functions are what I mainly program.
Other Languages
This feature is a .NET feature but the demos only use C# code. I was wondering if this also works on the other languages in the .NET ecosystem and so decided to test F# and VB.NET.
F#
I developed an F# API, which is a basic API, and to my surprise, it functions exactly the same way as the C# API. I only needed to add the following lines to the fsproj file:
Then run the following command:
DevOps
This is all great, but I won’t type the command into the terminal when I want to use CI/CD. However, there are no clear guides online, so I will make one for you.
I’m assuming that you have modified the csproj file as shown in the previous sections. The first task is always a docker login into your ACR:
Basic API release
For this demo, I will use the very basic weather forecast API that is created when you create a new .NET API. I will use the following task:
If you set publishWebProjects to “true”, it will publish all APIs in the solution. As you can see, there is nothing complicated about this. In the ACR you can see the following image:
220.46 MB is a nice size, but what if I want it to be alpine based but only support linux-x64? This you can find in the next section.
Advanced API release
The publish feature allows you to add a lot of arguments. In the following tasks, I included some that are useful in my opinion:
This will create a smaller image, with a size of 115.81 MB. Let’s go over the arguments:
- ContainerRepository: The name of the container (this overwrites the name in the csproj file).
- ContainerImageTags: This is a very tricky argument. You have both. ContainerImageTag and ContainerImageTags: If you pick the tags that the official documentation tells you to, you need to divide them with a semicolon. But they also tell you that in CI/CD cases you might need to do it like this: “1.0.0;latest”. This does not work on Azure DevOps, and after quite a bit of testing I found out that “\”1.0.0;latest\”” does work.
- Version: The version of the API.
- ContainerRegistry: The ACR.
- ContainerUser: The user that runs the container. I set it to root, but it is recommended to set it to a non-root user.
- ContainerFamily: The type of base image: I set this to alpine. You can also set the base image itself, but I recommend the family, because this will update with the .NET version of the project automatically.
- ContainerRuntimeIdentifier: The runtime identifier. I set this to linux-x64.
Private NuGet feed
After a talk with our Containerization Lead, I discovered that one of the things they need to deal with is private NuGet feeds. It is much easier to do that this way than with Dockerfiles. As we use the .NET build and then later package it into a Docker, we don’t need to deal with anything special: it is the same as any other .NET project. You just need to add the nuget.config, do a dotnet restore and then a dotnet publish. The Yaml pipeline looks like this:
Drawbacks
While this is a very easy way to containerize a .NET application, there are some drawbacks. The biggest is that you no longer make the Dockerfile, which reduces your knowledge of the underlying technology. You are also limited in your options. And while there are a lot of arguments you can add to the publish command, after a while you might ask whether it’s easier just to write a Dockerfile. It also abstracts the containerization process: in the Azure Function example, you can see that I set a ContainerBaseImage. But is this used as the run image or both the build and run image… or in another way altogether?
Future: .NET Aspire?
There might be another big shift coming in .NET containerization with .NET Aspire. I’m not yet an expert on this, but this method overrides the need for things like docker images, and container registries. You just make a .NET Aspire application and then you can generate a manifest JSON file based on the application. This manifest will describe the full application and all its dependencies, and then with the Azure developer CLI (azd) you can just run azd init & azd up.
Conclusion
This useful feature removes the need to update the Dockerfiles every time you execute a .NET bump, and is a very practised way of doing things for our .NET developers. However, I hope that they add the Azure Function support soon.
Thanks for reading! I hope this blog post was useful to you. If you have any questions or remarks, feel free to contact me.
Subscribe to our RSS feed