Rockford Lhotka

VP, Open Source Creator, Author, Speaker

Build Containers Without a Dockerfile

05 Jan 2023

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.

Finally, a final image is created based on base, and the contents from the temporary publish image’s /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.

Existing Tooling

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 Microsoft.NET.Build.Containers package.

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 dotnet CLI.

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.

Open the csproj file in your editor and add a ContainerImageName element to the PropertyGroup node:

    <ContainerImageName>webtest</ContainerImageName>

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:

    <ContainerImageTag>latest</ContainerImageTag>

The example shown here indicates that the container image will be labeled webtest:latest.

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 node:

  <ItemGroup>
    <ContainerPort Include="443" Type="tcp" />
  </ItemGroup>

ℹ️ For full details about the options you can set in your csproj file, 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

Because the 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.

The -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.

The -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.

Credits

Thank you to @timheuer and @ChetHusk for their help in troubleshooting some issues I encountered, and also explaining some behind-the-scenes details.

Resources

The code shown here is available in GitHub at rockfordlhotka/webtest.

The documentation for this feature is available on Microsoft Learn: Publish As Container.

Conclusion

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.