Containers are rapidly becoming the primary deployment model for server-side code, whether that be a web site, an app server, or other server-side code that you need to run in your environment.
Containers offer substantial value, because a container image is created at build time, and once created, the same container image can be used for dev testing, QA testing, and ultimately pushed into production. No need to keep rebuilding the project, or tweaking configuration, or other things that can cause inconsistencies in the CI/CD pipeline between a developer and production deployment.
As a general rule, container images are created using the Docker tool, and that tool uses a definition stored in a Dockerfile.
There’s a new .NET capability that allows the
dotnet command line tool to directly create a container image without Docker or a Dockerfile.
First, I will review how Docker has been used to create container images.
Quick Overview of a Dockerfile
Here is an example of a Dockerfile that the
docker build CLI command uses to create a Linux container image for a .NET project:
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["webtest/webtest.csproj", "webtest/"] RUN dotnet restore "webtest/webtest.csproj" COPY . . WORKDIR "/src/webtest" RUN dotnet build "webtest.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "webtest.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "webtest.dll"]
The specification defines a
base image using the ASP.NET 6.0 runtime, which will expose ports 80 and 443 when deployed.
It then defines a temporary container image named
build based on the .NET 6.0 SDK. This temporary image has access to the .NET compilers and other build tools necessary to build and publish the project. This image is used to run the
dotnet build command to compile the solution in release mode.
The next step is to define another temporary container image named
publish, based off the
build image. This image is used to run the
dotnet publish command to create the published output for the .NET project. The result is that the
/app/publish directory contains only the files necessary for the app to execute at runtime.
final image is created based on
base, and the contents from the temporary
/app/publish directory are copied into the final image’s
/app directory. The
ENTRYPOINT statement indicates that when this container image is executed, a
dotnet webtest.dll command will be run to start the app.
Visual Studio, Visual Studio Code, and .NET have supported the creation of container images for several years, starting with .NET Core. To build a container image from a .NET project, you add a
Dockerfile file to your project, and then your tooling (or you manually) runs the
docker build command to build your project based on the contents of that Dockerfile specification.
Visual Studio has the ability to create the
Dockerfile file for you, or you can create it by hand. Visual Studio is aware of your project’s support for containers, and so will execute a
docker build command for you, or you can run the command yourself at a command line interface (CLI) prompt.
Creating Images Without Docker or a Dockerfile
Starting with .NET 7 Microsoft has introduced a new feature that allows you to build a .NET project into a container image without adding a
Dockerfile file to your project.
This is nice, because it means you don’t need to worry about maintaining the
Dockerfile file, storing it in source control, seeing it in your project, or otherwise thinking about it.
On the other hand, it is not ideal because cloud-native infrastructure tooling you might use with images, such as Docker or docker-compose rely on the contents of a well-formed Dockerfile. What this means, in practice, is that you may need to continue to maintain a
Dockerfile file in your project.
For scenarios where you want to create a single microservice, web, or web API project, build it into a container image, and push the image into Azure or Kubernetes, this new .NET capability is useful.
I will walk through the steps you can follow to use this new feature.
Prerequisites and Constraints
As with the existing functionality from .NET Core to today, your development workstation must have Docker Desktop installed and running.
ℹ️ Docker is not used to create the image, but the tooling does attempt to push the image into the local Docker image repository once it has been created, and this is why Docker Desktop is required.
The current tooling only supports Linux container images.
I don’t find this to be a serious limitation, as my view on Windows containers is that they exist to support legacy software. I assume that any server-side code I create using modern .NET will end up running in a Linux environment.
This new feature requires .NET 7, so you must have the .NET 7 SDK installed.
If you are using Visual Studio, Visual Studio 2022 is required to work with .NET 7. This new tooling doesn’t yet work in Visual Studio, but you can still use Visual Studio as an editor as long as you are willing to use the CLI to run the
dotnet publish command.
If you are already using the CLI with something like Visual Studio Code, then your existing build process will be largely unaffected.
Create the Project
Create a .NET 7 Web project named
webtest, either in Visual Studio or using the CLI:
dotnet new web -o webtest
If using Visual Studio do not check the box in the project creation wizard that allows you to use Docker. Checking the box during the project creation causes Visual Studio to add a
Dockerfile file to the project, which is not what you want in this example.
Instead of adding a
Dockerfile file to the project, you will need to add a NuGet package to the project and edit the
csproj file to specify things like the container image name, tag, and ports used by the container.
Add a NuGet Reference
If using Visual Studio, right-click on the project’s Dependencies node in Solution Explorer and choose the option to manage NuGet references. Add a reference to the
If you are not using Visual Studio, change your directory to the
webtest folder and run the following CLI command:
dotnet add package Microsoft.NET.Build.Containers
The result is that your project now references the NuGet package required to support creating a container image directly from the
Edit the csproj File
When building a container image you can optionally specify the name of the image file, and the tag and port values. With this new technique, these values are included in the
csproj file in your editor and add a
ContainerImageName element to the
This specifies that the container image will be named
webtest. If you don’t provide this element, the container image name will default to the assembly name of the .NET project.
Optionally, you can also provide one or more tag values. By default the tooling will create a tag based on the version number of the project. If you want to override the default, add a
ContainerTag element or
ContainerTags element for multiple tags:
The example shown here indicates that the container image will be labeled
Because the project is a web app, also define the ports needed by the container with a
ContainerPort element. This element needs to be in its own
<ItemGroup> <ContainerPort Include="443" Type="tcp" /> </ItemGroup>
ℹ️ For full details about the options you can set in your
csprojfile, check out the Microsoft Learn publish as container document.
To recap, you now have a .NET 7 project that references the
Microsoft.NET.Build.Containers package, and which specifies container image information in the
csproj file. You can now build the project to create the container image.
Create the Container Image
To build the container image you must use the CLI. Change directory to the location of the
csproj file and run the following command (for web projects):
dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer -c Release
Notice that the target operating system is Linux, and the architecture is set to x64.
If your project is a console app or other non-web project, then the
dotnet publish command would be:
dotnet publish --os linux --arch x64 /t:PublishContainer -c Release
The output from the
dotnet publish command will look like this:
MSBuild version 17.4.0+18d5aef85 for .NET Determining projects to restore... Restored /home/rockylhotka/src/webtest/webtest.csproj (in 141 ms). webtest -> /home/rockylhotka/src/webtest/bin/Release/net7.0/linux-x64/webtest.dll webtest -> /home/rockylhotka/src/webtest/bin/Release/net7.0/linux-x64/publish/ Building image 'webtest' with tags latest on top of base image mcr.microsoft.com/dotnet/aspnet:7.0 Pushed container 'webtest:latest' to Docker daemon
Once the command has finished you can use the
docker image ls command to see the image:
REPOSITORY TAG IMAGE ID CREATED SIZE webtest latest 4e3ff779ff69 2 seconds ago 216MB
This is the same result as if you’d created a
Dockerfile file for the project and run the
docker build command, but without the need to create and maintain the Dockerfile asset. And the image was created without using Docker itself, so that dependency has been eliminated (except for the automatic push to the local Docker image repository).
Inspecting the Container Image
You can inspect the resulting image to see its configuration. This is done using the
docker CLI command:
docker image inspect 4e3ff779ff69
Notice that I’m using the
IMAGE ID value for the image shown by the
docker image ls command to specify which image I want to inspect. The output is JSON, and provides you with all the information about the image, such as
"Os": "linux" and many other details.
Running the Container Image
dotnet publish command pushed the image into the local Docker repository, you can use the
docker CLI command to easily run the image as a container on your local workstation:
docker run -d -P 4e3ff779ff69
Again, I’m using the image id to specify the image I want to run.
-d flag tells Docker to detach my CLI from the container. By default when you run a container your CLI window will remain connected to the console output from that container. As a result, your CLI window is no longer useful expect for watching debug output. The
-d flag frees up your CLI window for further use.
-P flag tells Docker to automatically map the ports listed in the container image to open ports on
localhost. In the
csproj file the
ContainerPort specified port 443, and so the
-P flag will map that port to some open port on my local computer.
You can find the port mapping information by using the
docker ps command. The result should look similar to this:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 083cccfcb1ad 4e3ff779ff69 "/app/webtest" 2 minutes ago Up 2 minutes 0.0.0.0:49155->443/tcp cool_lumiere
Notice the port mapping:
0.0.0.0:49155->443/tcp. This means that the container’s port 443 is mapped to my computer’s
localhost:49155 and I can use a browser to navigate to that address.
The code shown here is available in GitHub at rockfordlhotka/webtest.
The documentation for this feature is available on Microsoft Learn: Publish As Container.
I am a strong advocate for the use of container images to publish server-side (and increasingly some client-side) software. Images provide many benefits to developers, IT operations, DevOps, QA, and other roles involved in software development, deployment, and management.
The ability to create container images directly using
dotnet publish, without the need for Docker or a Dockerfile asset, is quite powerful in some scenarios.
This new feature is, I believe, another small step toward overall better tooling and workflows for developers and IT that we are seeing from Microsoft and the .NET ecosystem.