.NET 8 Zero to Hero Series is LIVE! 🚀Join Now

16 min read

Built-In Container Support for .NET 7 - Dockerize .NET Applications without Dockerfile!

#dotnet #docker

In this article, we will learn about the fantastic new Built-In Container Support for .NET, starting from .NET 7 and above. The all-new .NET 7 SDK now allows you to build Docker Images & spin up containers for your application in no time, without the need for an additional Dockerfile (One less file to maintain!). Let’s get started.

Note that this is not a tutorial for Docker! This article assumes that you already have a working idea with Docker and containers. If you are new to the docker world, let me know in the comments section below, and I will try to draft a detailed article about Dockerising .NET Applications and various other related concepts.

Microsoft has announced that starting from the .NET 7 SDK, the framework will support building containerized applications within the publish tooling, which bypasses the need to have an additional Dockerfile itself. Now, this is going to help reduce the Docker code that the developer has to maintain and makes the entire workflow much simpler and easier than before.

Basic idea

So, what’s the general workflow of most Software Applications (Especially Microservices)? Code, Push to a Repository, Run a CI/CD Pipeline, as part of which there would be a docker build step which would usually read from the Long Dockerfile and generate Docker Images for the application, and finally, the Image is deployed to some cloud computing service. Now that you no longer have to maintain the Dockerfile, the .NET framework internally would generate an Image for you, which you can push into an Image Repository of your choice.

As part of this article, we will learn about building Docker Images using the .NET CLI Tool, explore the variety of options it exposes, compare it with the Dockerfile approach, and also integrate this into a sample Github Actions Workflow, to demonstrate how useful this can be for your next project.

First up, let’s see quickly how you can Dockerize a .NET 6 Application using the Dockerfile.

For a change, let’s use Visual Code for development this time. For developing fullstackhero dotnet-microservices-boilerplate, I have been using Visual Code 90% of the time and I find it really comfortable since it’s much light-weight compared to Visual Studio, and it also has a pretty RAW vibe while developing. I should maybe write in the extensions required for developing .NET applications in Visual Code. Let me know in the comments section below.

For ease of understanding, we will be building two simple applications, dotnet6, and dotnet7. I will place the code of these applications within this same repository under the respective folders. Later in this article, I will also write a simple GitHub Actions workflow, directly on GitHub to show how this can be integrated within your CI/CD pipeline and the image pushed to DockerHub for instance.

Ensure that you have both of the SDKs installed on your machine along with the Docker Desktop.

I created a new repository over at GitHub, cloned it to my local development machine, and opened up the repository folder using Visual Code. Here, let me add a new folder named dotnet6.

You can find the source code of this implementation here.

Dockerizing .NET 6 Applications - In Short

Once created, let’s navigate into the dotnet6 folder and create a simple .NET 6 Web API Project using dotnet CLI commands. Simply run the following.

dotnet new webapi --name HelloDocker --framework net6.0

This should create a new .NET WebAPI project for you with HelloDocker as the project name pointing to a TargetFramework of net6.0.

Feel free to skip this section if you are already well aware of dockerizing .NET 6 and below applications.

So, this is a very simple web API that returns the standard weather data like any other default new ASP.NET Core WebAPI project. Let’s dockerize this! Upto .NET 6, we had to add a Dockerfile file for this at the root of the project.

Quick Tip: First thing I like to do once I create any new dotnet application is to clean up the launchSettings.json and remove all the IIS-related configurations as I don’t use it.

{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"HelloDocker": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7290;http://localhost:5033",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

From here, I will know that my API will always run on ports 7290 (secure) and 5033(HTTP). Note that this is applicable only when you run your application on your development machine and not on the Docker Container. By default, when you spin up a docker container with a .NET Image, the application would run at http://+:80. You can override this to a different port number in Docker Container by setting the following Environment Variable.

Let’s add a new file and name it Dockerfile. The following will be its content.

FROM mcr.microsoft.com/dotnet/sdk:6.0 as build-env
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /publish
FROM mcr.microsoft.com/dotnet/aspnet:6.0 as runtime
WORKDIR /publish
COPY --from=build-env /publish .
ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000
ENTRYPOINT ["dotnet", "HelloDocker.dll"]

Here is what would happen in general. Docker would pull the dotnet6 SDK from Microsoft’s Image Repository, Copy the HelloDocker.csproj file into the build directory internally, run dotnet-restore command to restore all the application dependencies, copy all the other content and run the dotnet publish command in Release Mode. Once that’s done, for executing the application, it pulls in the runtime image of aspnet:6.0, and exposes a few ports like 5033, and 7290 which we saw earlier in the launchsettings.json. And finally set the entry point of the application as HelloDocker.dll.

Note that you can also auto-generate this Dockerfile with Visual Studio at the time of project creation. You would just have to enable Docker Support and select Linux, and Visual Studio would add a Dockerfile for you at the root of your new project. This may or may not interest you, as you will have to modify this anyways with the other changes in your project structure.

Now that the Dockerfile is ready, let’s build the docker image by running the following command from the folder where the Dockerfile file exists :

docker build -t hellodockerfrom6 .

This will initialize the docker build process. Once done, it should push an image named hellodockerfrom6 to your local docker instance. To verify this, open up Docker Desktop and switch to the Images tab. You should be seeing something like this.

built-in-container-support-for-dotnet-7

From here, you can spin up a Docker Container using the hellodockerfrom6 Image and pass on Optional Settings, Port Numbers, and Environment Variables as required. I mapped the local 5000 port to the 5000 port of the Docker container. In this way, any traffic/request sent to the 5000 port of localhost would be redirected to internal port 5000 of the docker container, which is our hellofromdocker6 application.

built-in-container-support-for-dotnet-7

That’s it. Once the container is up and running, just navigate to http://localhost:5000/WeatherForecast/, and you would be seeing the following weather data as the response.

built-in-container-support-for-dotnet-7

What Might Make Life Tougher?

As you might have seen, this is already an easy way to build docker images from .NET Applications. But, as soon as your .NET Solution grows in project size and complexities, this can result in a messy Dockerfile file. For instance, here is the Dockerfile from one of my solutions which has almost 10+ projects referenced.

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /
COPY ["Directory.Build.props", "/"]
COPY ["Directory.Build.targets", "/"]
COPY ["dotnet.ruleset", "/"]
COPY ["stylecop.json", "/"]
COPY ["src/Host/Host.csproj", "src/Host/"]
COPY ["src/Core/Application/Application.csproj", "src/Core/Application/"]
COPY ["src/Core/Domain/Domain.csproj", "src/Core/Domain/"]
COPY ["src/Core/Shared/Shared.csproj", "src/Core/Shared/"]
COPY ["src/Infrastructure/Infrastructure.csproj", "src/Infrastructure/"]
COPY ["src/Migrators/Migrators.MSSQL/Migrators.MSSQL.csproj", "src/Migrators/Migrators.MSSQL/"]
COPY ["src/Migrators/Migrators.MySQL/Migrators.MySQL.csproj", "src/Migrators/Migrators.MySQL/"]
COPY ["src/Migrators/Migrators.PostgreSQL/Migrators.PostgreSQL.csproj", "src/Migrators/Migrators.PostgreSQL/"]
COPY ["src/Migrators/Migrators.Oracle/Migrators.Oracle.csproj", "src/Migrators/Migrators.Oracle/"]
RUN dotnet restore "src/Host/Host.csproj" --disable-parallel
COPY . .
WORKDIR "/src/Host"
RUN dotnet publish "Host.csproj" -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app/publish .
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser
ENV ASPNETCORE_URLS=https://+:5050;http://+:5060
EXPOSE 5050
EXPOSE 5060
ENTRYPOINT ["dotnet", "FSH.WebApi.Host.dll"]

You can understand how messy this is going to be for maintenance, right? Especially in cases for Microservices, where you will have to maintain 1 Dockerfile for each of the services, which internally references multiple CSPROJs.

To make this situation simpler and easier to manage, Microsoft has improved the Container support starting from .NET 7, where you no longer need a Dockerfile to actually Dockerize your application. However, note that the Dockerfile approach would still continue to work.

Another eventual issue is related to the docker build context. Dockerfile assumes that everything it needs would be in the same folder as the Dockerfile, which is apparently not the case when you build applications with multiple layers or slices. To get a better understanding, let’s say in a random Microservices solution, one of your projects has Dockerfile at the root. While you build a Docker Image, it’s easy to miss including other files which are at the root of the solution. Thus it’s a very common scenario for docker to miss files like Directory.Packages.props which are quite important for the build process.

Built-In Container Support for .NET 7

Microsoft has addressed these specific issues. With the new update, you no longer have to worry about the gaps in contexts as well, which I am pretty sure most of you have already faced!

Let’s see how the new approach is, and explore it.

For this, let’s create a new folder at the root of the repository and name it dotnet7, and create a new web API 7.0 using the following command.

dotnet new webapi --name HelloDocker7 --framework net7.0

Now, as I said earlier, it’s no longer necessary to add a Dockerfile. Rather, simply navigate to the project and add a new NuGet package using the following command.

dotnet add package Microsoft.NET.Build.Containers

Note that this is just a temporary package as Microsoft claims, and will be bundled along with the SDK in further .NET releases. More or less, this is the package reference that you would need now to create the Docker Image for you.

Literally, this is the only thing you will have to add to get a basic Docker Image.

Now, all we have to do is to run a dotnet publish command along with a couple of parameters, and your docker image should be available locally! Let’s check this out.

At the root of the project, run the following command.

dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer -p:ContainerImageName=hellodocker7

Let’s go through each of the arguments and parameters that we have passed here. Note that these are the basic parameters only. This approach also allows you to still have all the customizations you had earlier with the Dockerfile. Let’s understand the basic command argument first.

  • —os : Specifies the targeting Operating system, which is Linux in our case.
  • —arch: Specifies the target architecture, which is x64 in our case.
  • -p denotes the specific parameters we are passing to this publish command.

Run the command.

built-in-container-support-for-dotnet-7

Once the Container Image is pushed, open up Docker Desktop to see your new image.

built-in-container-support-for-dotnet-7

You can further simplify the command by adding certain parameters right into the csproj file! Open up the HelloDocker7.csproj file on your Visual Code and the following lines of code.

<PropertyGroup>
<ContainerImageName>hellodocker7</ContainerImageName>
<PublishProfile>DefaultContainer</PublishProfile>
<ContainerImageTags>1.1.0;latest</ContainerImageTags>
</PropertyGroup>

So, what this does is, sets the ContainerImageName, PublishProfile, and ContainerImageTag, so that you really don’t have to add this into your terminal arguments every time! This time, run the following command.

dotnet publish --os linux --arch x64

You can further simplify this command by adding the following property as well.

<RuntimeIdentifier>linux-x64</RuntimeIdentifier>

And all you need is this simple dotnet command!

dotnet publish

Once done, you will be able to see the new image with tag 1.1.0 on your DockerDesktop. This way you will be able to specify different customizations for your Docker Image directly from your csproj file.

Additional Customizations for Built-In Docker Support.

Dockerfile provided us with a lot of flexibility in customizing the Image. This built-in approach also makes no compromise for it. Here are a couple of additional customizations you might be already looking for while using the Built-In Docker Support for .NET! For instance, how does this approach determine the base image, how do I add environment variables, how do I change the tags, and so on?

  • ContainerBaseImage: This property allows you to control the base image used for building your dotnet application. By default, the SDK assumes certain values which will be mcr.microsoft.com/dotnet/aspnet for ASP.NET Core Projects.
  • ContainerImageName: In case you wanted to switch the Image Name. If this is not specified, the SDK will fall back to the name of the assembly itself.
  • ContainerPort: For automatic port mapping.

And here is how you would specify Environment Variables.

<ItemGroup>
<ContainerEnvironmentVariable Include="ENABLE_REDIS" Value="Trace" />
</ItemGroup>

Here is a detailed documentation of other parameters/properties that you could use for customizing your Container: https://github.com/dotnet/sdk-container-builds/blob/main/docs/ContainerCustomization.md.

Directory.Build.props

In most cases, you will be having multiple C# projects under a solution. For such scenarios, it’s also possible to include all these Properties within the Directory.Build.props (at the root of the solution), so that each of the referenced projects can still use these properties.

For example, this is a snippet of my Directory.Build.props file from the dotnet microservices boilerplate solution.

<PropertyGroup>
<PublishProfile>DefaultContainer</PublishProfile>
<ContainerImageTags>1.3.0;latest</ContainerImageTags>
</PropertyGroup>

And at the csproj level, I specify the following.

<PropertyGroup>
<ContainerImageName>fsh-microservices.catalog</ContainerImageName>
</PropertyGroup>

This way, it’s super easy to maintain specific and shared properties over the long run.

For Local Development

This can be well suited for local development when you frequently want to containerize your .NET applications every once in a while. Just a simple dotnet publish would make your Docker image available readily for you even without worrying “Do I have to make some changes on my Dockerfile Since I have changed quite a bit and added a couple of new project references.”

Additionally, you can take advantage of Visual Code’s tasks to make life easier for you. Something similar to the below code snippet from my .vscode/tasks.json file.

{
"label": "publish:catalog-api",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/services/catalog/Host/Host.csproj",
"--os",
"linux",
"--arch",
"x64",
"-c",
"Release",
"-p:PublishProfile=DefaultContainer",
"-p:ContainerImageTags=latest",
"--self-contained"
],
"problemMatcher": "$msCompile"
},

GitHub Actions Workflow

Here is an interesting section, where we will push the .NET application to our repository, create a GitHub action to build the .NET 7 application, publish it (build the docker image), and finally push it to a public DockerHub Repository.

Switch to the Actions tab on your GitHub repository and create a new blank workflow and name your new file something like ci.yml

built-in-container-support-for-dotnet-7

Here is a very simple ci pipeline action workflow that I have written, which can build your .NET 7 application, generate a docker image for it, and finally push it to docker Hub.

name: ci
on:
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Restore dependencies
working-directory: ./dotnet7/HelloDocker7/
run: dotnet restore
- name: Build
working-directory: ./dotnet7/HelloDocker7/
run: dotnet build --no-restore
docker:
name: Docker Build & Push
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker Login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build Image
working-directory: ./dotnet7/HelloDocker7/
run: dotnet publish -c Release -p:ContainerImageName=${{ secrets.DOCKER_USERNAME }}/hellodockerfromdotnet7
- name: Push Image
run: docker push ${{ secrets.DOCKER_USERNAME }}/hellodockerfromdotnet7 --all-tags

Let’s go through the above script in a bit more detail.

  • Line 2-3: This specifies that this workflow can only be triggered manually. There are several other options to automatically trigger this workflow, like on every push to the repository. For now, the Manual trigger is good enough for us in this demo.
  • Lines 5-20: Based on .NET 7, the script would restore the HelloDocker7 dependencies and attempt to build them.
  • Lines 20-37: Relates to all things docker.
  • Lines 28-32: Attempts to log in to DockerHub using the username/password set as the repository secret (Screenshot attached in upcoming sections)
  • Line 35: We are running our favorite new dotnet publish command with Release configuration. Remember that this will also generate a DockerImage using the specified ContainerImageName.
  • Line 37: Whatever image we have built in the previous step will be pushed to the docker hub in this step.

To set a repository secret, open up the repository on GitHub and navigate to the settings tab. Here select the Secrets and add the required repository secrets. In our case, we need to set the docker username and password so that it can be accessed by the pipeline.

built-in-container-support-for-dotnet-7

Once that’s done, save the yml file and head to the docker hub. Here, create a new public repository and name it the same as what you have mentioned in the yml file. In my case, I had to create a repository with the name hellodockerfromdotnet7. This ensures that the pipeline script will be able to push to this repository.

built-in-container-support-for-dotnet-7

With all that done, navigate back to Github, your new repository, and head over to the Actions tab. Here, select your workflow, which in this case is ‘ci’. Click on Run Workflow.

built-in-container-support-for-dotnet-7

You can see that the pipeline is up and running now, and attempting to build your dotnet application for you.

built-in-container-support-for-dotnet-7

In the Docker Build & Push step, this is what we were talking about earlier. Our new dotnet publish in action!

built-in-container-support-for-dotnet-7

built-in-container-support-for-dotnet-7

If things go as expected, you will be seeing greens everywhere, meaning your docker image should now be pushed to DockerHub as well.

built-in-container-support-for-dotnet-7

As expected, you can see that our Images are now pushed to the docker Hub! This is how easily you can integrate the dotnet publish command within the pipeline as well.

Minor Limitations

There are a couple of limitations to this.

  • docker-compose needs a Dockerfile to build the image for you. This is currently not supported. But, you can get around this by pushing the docker image to your local development machine, and then referring to this new image in the docker-compose file.
  • Only x64-based Linux images are supported as of now.
  • There is no built-in support for authentication to external image repositories.

Since this is an actively developed project, all of these limitations would be addressed quite soon by Microsoft. Apart from that, there are no major issues as of now that would stop us from using this awesome new feature!

That’s a wrap for this article. Hope you enjoyed it.

Summary

In this article, we learned about the all-new built-in container support for .NET applications starting from the .NET 7 SDK. We went through the Standard Dockerfile approach for .NET 6 applications (which is still applicable for the newer .NET SDKs), discussed about the issues with the Dockerfile, went through the new SDK feature of .NET that no longer needs a Dockerfile for you to build docker images. Here, we went through various customizations and parameters that are available and discussed how this can be used easily for local development purposes. Finally, we built a GitHub Action Workflow that would build the application, create a docker image, and finally push it to DockerHub!

You can find the code used for this demonstration here on my GitHub - https://github.com/iammukeshm/built-in-container-support-for-dotnet-7

Make sure to share this article with your colleagues if it helped you! Helps me get more eyes on my blog as well. Thanks!

Stay Tuned. You can follow this newsletter to get notifications when I publish new articles – https://newsletter.codewithmukesh.com/subscribe. Do share this article with your colleagues and dev circles if you found this interesting. Thanks!

Source Code ✌️
Grab the source code of the entire implementation by clicking here. Do Follow me on GitHub .
Support ❤️
If you have enjoyed my content and code, do support me by buying a couple of coffees. This will enable me to dedicate more time to research and create new content. Cheers!
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.

Supercharge your .NET Skills 🚀

I have started a .NET 8 Zero to Hero Series. We will cover everything from the basics to advanced topics to help you with your .NET Journey!

Join Now

No spam ever, we are care about the protection of your data. Read our Privacy Policy