Containerize a .NET app with dotnet publish
Containers have many features and benefits, such as being an immutable infrastructure, providing a portable architecture, and enabling scalability. The image can be used to create containers for your local development environment, private cloud, or public cloud. In this tutorial, you learn how to containerize a .NET application using the dotnet publish command without the use of a Dockerfile. Additionally, you explore how to configure the container image and execution, and how to clean up resources.
Prerequisites
Install the following prerequisites:
- .NET 8+ SDK
If you have .NET installed, use thedotnet --info
command to determine which SDK you're using. - Docker Community Edition
In addition to these prerequisites, it's recommended that you're familiar with Worker Services in .NET.
Create .NET app
You need a .NET app to containerize, so start by creating a new app from a template. Open your terminal, create a working folder (sample-directory) if you haven't already, and change directories so that you're in it. In the working folder, run the following command to create a new project in a subdirectory named Worker:
dotnet new worker -o Worker -n DotNet.ContainerImage
Your folder tree looks like the following:
📁 sample-directory
└──📂 Worker
├──appsettings.Development.json
├──appsettings.json
├──DotNet.ContainerImage.csproj
├──Program.cs
├──Worker.cs
└──📂 obj
├── DotNet.ContainerImage.csproj.nuget.dgspec.json
├── DotNet.ContainerImage.csproj.nuget.g.props
├── DotNet.ContainerImage.csproj.nuget.g.targets
├── project.assets.json
└── project.nuget.cache
The dotnet new
command creates a new folder named Worker and generates a worker service that, when run, logs a message every second. From your terminal session, change directories and navigate into the Worker folder. Use the dotnet run
command to start the app.
dotnet run
Building...
info: DotNet.ContainerImage.Worker[0]
Worker running at: 10/18/2022 08:56:00 -05:00
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: .\Worker
info: DotNet.ContainerImage.Worker[0]
Worker running at: 10/18/2022 08:56:01 -05:00
info: DotNet.ContainerImage.Worker[0]
Worker running at: 10/18/2022 08:56:02 -05:00
info: DotNet.ContainerImage.Worker[0]
Worker running at: 10/18/2022 08:56:03 -05:00
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
Attempting to cancel the build...
The worker template loops indefinitely. Use the cancel command Ctrl+C to stop it.
Add NuGet package
Starting with .NET SDK version 8.0.200, the PublishContainer
target is available for every project. To avoid depending on the Microsoft.NET.Build.Containers
NuGet package, ensure that you're using the latest .NET SDK version. Additionally, your project file needs to have IsPublishable
set to true
and enable SDK container support.
Important
By default, the IsPublishable
property is set to true
for console
, webapp
, and worker
templates.
To enable SDK container support, set the EnableSdkContainerSupport
property to true
in your project file.
<PropertyGroup>
<IsPublishable>true</IsPublishable>
<EnableSdkContainerSupport>true</EnableSdkContainerSupport>
</PropertyGroup>
Set the container image name
There are various configuration options available when publishing an app as a container.
By default, the container image name is the AssemblyName
of the project. If that name is invalid as a container image name, you can override it by specifying a ContainerRepository
as shown in the following project file:
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-DotNet.ContainerImage-2e40c179-a00b-4cc9-9785-54266210b7eb</UserSecretsId>
<ContainerRepository>dotnet-worker-image</ContainerRepository>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
</ItemGroup>
</Project>
For more information, see ContainerRepository.
Publish .NET app
To publish the .NET app as a container, use the following dotnet publish command:
dotnet publish --os linux --arch x64 /t:PublishContainer
The preceding .NET CLI command publishes the app as a container:
- Targeting Linux as the OS (
--os linux
). - Specifying an x64 architecture (
--arch x64
).
Important
To build the container locally, you must have the Docker daemon running. If it isn't running when you attempt to publish the app as a container, you'll experience an error similar to the following:
..\build\Microsoft.NET.Build.Containers.targets(66,9): error MSB4018:
The "CreateNewImage" task failed unexpectedly. [..\Worker\DotNet.ContainerImage.csproj]
The command produces output similar to the example output:
Determining projects to restore...
All projects are up-to-date for restore.
DotNet.ContainerImage -> .\Worker\bin\Release\net8.0\linux-x64\DotNet.ContainerImage.dll
DotNet.ContainerImage -> .\Worker\bin\Release\net8.0\linux-x64\publish\
Building image 'dotnet-worker-image' with tags latest on top of base image mcr.microsoft.com/dotnet/aspnet:8.0
Pushed container 'dotnet-worker-image:latest' to Docker daemon
This command compiles your worker app to the publish folder and pushes the container to your local docker registry.
Configure container image
You can control many aspects of the generated container through MSBuild properties. In general, if you can use a command in a Dockerfile to set some configuration, you can do the same via MSBuild.
Note
The only exceptions to this are RUN
commands. Due to the way containers are built, those cannot be emulated. If you need this functionality, you'll need to use a Dockerfile to build your container images.
ContainerArchiveOutputPath
Starting in .NET 8, you can create a container directly as a tar.gz archive. This feature is useful if your workflow isn't straightforward and requires that you, for example, run a scanning tool over your images before pushing them. Once the archive is created, you can move it, scan it, or load it into a local Docker toolchain.
To publish to an archive, add the ContainerArchiveOutputPath
property to your dotnet publish
command, for example:
dotnet publish \
-p PublishProfile=DefaultContainer \
-p ContainerArchiveOutputPath=./images/sdk-container-demo.tar.gz
You can specify either a folder name or a path with a specific file name. If you specify the folder name, the filename generated for the image archive file will be $(ContainerRepository).tar.gz
. These archives can contain multiple tags inside them, only as single file is created for all ContainerImageTags
.
Container image naming configuration
Container images follow a specific naming convention. The name of the image is composed of several parts, the registry, optional port, repository, and optional tag and family.
REGISTRY[:PORT]/REPOSITORY[:TAG[-FAMILY]]
For example, consider the fully qualified mcr.microsoft.com/dotnet/runtime:8.0-alpine
image name:
mcr.microsoft.com
is the registry (and in this case represents the Microsoft container registry).dotnet/runtime
is the repository (but some consider this theuser/repository
).8.0-alpine
is the tag and family (the family is an optional specifier that helps disambiguate OS packaging).
Some properties described in the following sections correspond to managing parts of the generated image name. Consider the following table that maps the relationship between the image name and the build properties:
Image name part | MSBuild property | Example values |
---|---|---|
REGISTRY[:PORT] |
ContainerRegistry |
mcr.microsoft.com:443 |
PORT |
ContainerPort |
:443 |
REPOSITORY |
ContainerRepository |
dotnet/runtime |
TAG |
ContainerImageTag |
8.0 |
FAMILY |
ContainerFamily |
-alpine |
The following sections describe the various properties that can be used to control the generated container image.
ContainerBaseImage
The container base image property controls the image used as the basis for your image. By default, the following values are inferred based on the properties of your project:
- If your project is self-contained, the
mcr.microsoft.com/dotnet/runtime-deps
image is used as the base image. - If your project is an ASP.NET Core project, the
mcr.microsoft.com/dotnet/aspnet
image is used as the base image. - Otherwise the
mcr.microsoft.com/dotnet/runtime
image is used as the base image.
The tag of the image is inferred to be the numeric component of your chosen TargetFramework
. For example, a project targeting net6.0
results in the 6.0
tag of the inferred base image, and a net7.0-linux
project uses the 7.0
tag, and so on.
If you set a value here, you should set the fully qualified name of the image to use as the base, including any tag you prefer:
<PropertyGroup>
<ContainerBaseImage>mcr.microsoft.com/dotnet/runtime:8.0</ContainerBaseImage>
</PropertyGroup>
Starting with .NET SDK version 8.0.200, the ContainerBaseImage
inference has been improved to optimize the size and security:
- Targeting the
linux-musl-x64
orlinux-musl-arm64
Runtime Identifiers, automatically chooses thealpine
image variants to ensure your project runs:- If the project uses
PublishAot=true
then thenightly/runtime-deps
jammy-chiseled-aot
variant of the base image for best size and security. - If the project uses
InvariantGlobalization=false
then the-extra
variants is used to ensure localization still works.
- If the project uses
For more information regarding the image variants sizes and characteristics, see .NET 8.0 Container Image Size Report.
ContainerFamily
Starting with .NET 8, you can use the ContainerFamily
MSBuild property to choose a different family of Microsoft-provided container images as the base image for your app. When set, this value is appended to the end of the selected TFM-specific tag, changing the tag provided. For example, to use the Alpine Linux variants of the .NET base images, you can set ContainerFamily
to alpine
:
<PropertyGroup>
<ContainerFamily>alpine</ContainerFamily>
</PropertyGroup>
The preceding project configuration results in a final tag of 8.0-alpine
for a .NET 8-targeting app.
This field is free-form, and often can be used to select different operating system distributions, default package configurations, or any other flavor of changes to a base image. This field is ignored when ContainerBaseImage
is set. For more information, see .NET container images.
ContainerRuntimeIdentifier
The container runtime identifier property controls the operating system and architecture used by your container if your ContainerBaseImage supports more than one platform. For example, the mcr.microsoft.com/dotnet/runtime
image currently supports linux-x64
, linux-arm
, linux-arm64
and win10-x64
images all behind the same tag, so the tooling needs a way to be told which of these versions you intend to use. By default, this is set to the value of the RuntimeIdentifier
that you chose when you published the container. This property rarely needs to be set explicitly - instead use the -r
option to the dotnet publish
command. If the image you've chosen doesn't support the RuntimeIdentifier
you've chosen, results in an error that describes the RuntimeIdentifiers the image does support.
You can always set the ContainerBaseImage
property to a fully qualified image name, including the tag, to avoid needing to use this property at all.
<PropertyGroup>
<ContainerRuntimeIdentifier>linux-arm64</ContainerRuntimeIdentifier>
</PropertyGroup>
For more information regarding the runtime identifiers supported by .NET, see RID catalog.
ContainerRegistry
The container registry property controls the destination registry, the place that the newly created image will be pushed to. By default it's pushed to the local Docker daemon, but you can also specify a remote registry. When using a remote registry that requires authentication, you authenticate using the well-known docker login
mechanisms. For more information, See authenticating to container registries for more details. For a concrete example of using this property, consider the following XML example:
<PropertyGroup>
<ContainerRegistry>registry.mycorp.com:1234</ContainerRegistry>
</PropertyGroup>
This tooling supports publishing to any registry that supports the Docker Registry HTTP API V2. This includes the following registries explicitly (and likely many more implicitly):
- Azure Container Registry
- Amazon Elastic Container Registry
- Google Artifact Registry
- Docker Hub
- GitHub Packages
- GitLab-hosted Container Registry
- Quay.io
For notes on working with these registries, see the registry-specific notes.
ContainerRepository
The container repository is the name of the image itself, for example, dotnet/runtime
or my-app
. By default, the AssemblyName
of the project is used.
<PropertyGroup>
<ContainerRepository>my-app</ContainerRepository>
</PropertyGroup>
Image names consist of one or more slash-delimited segments, each of which can only contain lowercase alphanumeric characters, periods, underscores, and dashes, and must start with a letter or number. Any other characters result in an error being thrown.
ContainerImageTag(s)
The container image tag property controls the tags that are generated for the image. To specify a single tag use ContainerImageTag
and for multiple tags use ContainerImageTags
.
Important
When you use ContainerImageTags
, you'll end up with multiple images, one per unique tag.
Tags are often used to refer to different versions of an app, but they can also refer to different operating system distributions, or even different configurations.
Starting with .NET 8, when a tag isn't provided the default is latest
.
To override the default, specify either of the following:
<PropertyGroup>
<ContainerImageTag>1.2.3-alpha2</ContainerImageTag>
</PropertyGroup>
To specify multiple tags, use a semicolon-delimited set of tags in the ContainerImageTags
property, similar to setting multiple TargetFrameworks
:
<PropertyGroup>
<ContainerImageTags>1.2.3-alpha2;latest</ContainerImageTags>
</PropertyGroup>
Tags can only contain up to 127 alphanumeric characters, periods, underscores, and dashes. They must start with an alphanumeric character or an underscore. Any other form results in an error being thrown.
Note
When using ContainerImageTags
, the tags are delimited by a ;
character. If you're calling dotnet publish
from the command line (as is the case with most CI/CD environments), you'll need to outer wrap the values in a single '
and inner wrap with double quotes "
, for example (='"tag-1;tag-2"'
). Consider the following dotnet publish
command:
dotnet publish -p ContainerImageTags='"1.2.3-alpha2;latest"'
This results in two images being generated: my-app:1.2.3-alpha2
and my-app:latest
.
Tip
If you experience issues with the ContainerImageTags
property, consider scoping an environment variable ContainerImageTags
instead:
ContainerImageTags='1.2.3;latest' dotnet publish
ContainerLabel
The container label adds a metadata label to the container. Labels have no impact on the container at run time, but are often used to store version and authoring metadata for use by security scanners and other infrastructure tools. You can specify any number of container labels.
The ContainerLabel
node has two attributes:
Include
: The key of the label.Value
: The value of the label (this may be empty).
<ItemGroup>
<ContainerLabel Include="org.contoso.businessunit" Value="contoso-university" />
</ItemGroup>
For a list of labels that are created by default, see default container labels.
Configure container execution
To control the execution of the container, you can use the following MSBuild properties.
ContainerWorkingDirectory
The container working directory node controls the working directory of the container, the directory that commands are executed within if not other command is run.
By default, the /app
directory value is used as the working directory.
<PropertyGroup>
<ContainerWorkingDirectory>/bin</ContainerWorkingDirectory>
</PropertyGroup>
ContainerPort
The container port adds TCP or UDP ports to the list of known ports for the container. This enables container runtimes like Docker to map these ports to the host machine automatically. This is often used as documentation for the container, but can also be used to enable automatic port mapping.
The ContainerPort
node has two attributes:
Include
: The port number to expose.Type
: Defaults totcp
, valid values are eithertcp
orudp
.
<ItemGroup>
<ContainerPort Include="80" Type="tcp" />
</ItemGroup>
Starting with .NET 8, the ContainerPort
is inferred when not explicitly provided based on several well-known ASP.NET environment variables:
ASPNETCORE_URLS
ASPNETCORE_HTTP_PORTS
ASPNETCORE_HTTPS_PORTS
If these environment variables are present, their values are parsed and converted to TCP port mappings. These environment variables are read from your base image, if present, or from the environment variables defined in your project through ContainerEnvironmentVariable
items. For more information, see ContainerEnvironmentVariable.
ContainerEnvironmentVariable
The container environment variable node allows you to add environment variables to the container. Environment variables are accessible to the app running in the container immediately, and are often used to change the run-time behavior of the running app.
The ContainerEnvironmentVariable
node has two attributes:
Include
: The name of the environment variable.Value
: The value of the environment variable.
<ItemGroup>
<ContainerEnvironmentVariable Include="LOGGER_VERBOSITY" Value="Trace" />
</ItemGroup>
For more information, see .NET environment variables.
Configure container commands
By default, the container tools launch your app using either the generated AppHost binary for your app (if your app uses an AppHost), or the dotnet
command plus your app's DLL.
However, you can control how your app is executed by using some combination of ContainerAppCommand
, ContainerAppCommandArgs
, ContainerDefaultArgs
, and ContainerAppCommandInstruction
.
These different configuration points exist because different base images use different combinations of the container ENTRYPOINT
and COMMAND
properties, and you want to be able to support all of them. The defaults should be useable for most apps, but if you want to customize your app launch behavior you should:
- Identify the binary to run and set it as
ContainerAppCommand
- Identify which arguments are required for your application to run and set them as
ContainerAppCommandArgs
- Identify which arguments (if any) are optional and could be overridden by a user and set them as
ContainerDefaultArgs
- Set
ContainerAppCommandInstruction
toDefaultArgs
For more information, see the following configuration items.
ContainerAppCommand
The app command configuration item is the logical entry point of your app. For most apps, this is the AppHost, the generated executable binary for your app. If your app doesn't generate an AppHost, then this command will typically be dotnet <your project dll>
. These values are applied after any ENTRYPOINT
in your base container, or directly if no ENTRYPOINT
is defined.
The ContainerAppCommand
configuration has a single Include
property, which represents the command, option, or argument to use in the entrypoint command:
<ItemGroup Label="ContainerAppCommand Assignment">
<!-- This is how you would start the dotnet ef tool in your container -->
<ContainerAppCommand Include="dotnet" />
<ContainerAppCommand Include="ef" />
<!-- This shorthand syntax means the same thing, note the semicolon separating the tokens. -->
<ContainerAppCommand Include="dotnet;ef" />
</ItemGroup>
ContainerAppCommandArgs
This app command args configuration item represents any logically required arguments for your app that should be applied to the ContainerAppCommand
. By default, none are generated for an app. When present, the args are applied to your container when it's run.
The ContainerAppCommandArgs
configuration has a single Include
property, which represents the option or argument to apply to the ContainerAppCommand
command.
<ItemGroup>
<!-- Assuming the ContainerAppCommand defined above,
this would be the way to force the database to update.
-->
<ContainerAppCommandArgs Include="database" />
<ContainerAppCommandArgs Include="update" />
<!-- This is the shorthand syntax for the same idea -->
<ContainerAppCommandArgs Include="database;update" />
</ItemGroup>
ContainerDefaultArgs
This default args configuration item represents any user-overridable arguments for your app. This is a good way to provide defaults that your app might need to run in a way that makes it easy to start, yet still easy to customize.
The ContainerDefaultArgs
configuration has a single Include
property, which represents the option or argument to apply to the ContainerAppCommand
command.
<ItemGroup>
<!-- Assuming the ContainerAppCommand defined above,
this would be the way to force the database to update.
-->
<ContainerDefaultArgs Include="database" />
<ContainerDefaultArgs Include="update" />
<!-- This is the shorthand syntax for the same idea -->
<ContainerDefaultArgs Include="database;update" />
</ItemGroup>
ContainerAppCommandInstruction
The app command instruction configuration helps control the way the ContainerEntrypoint
, ContainerEntrypointArgs
, ContainerAppCommand
, ContainerAppCommandArgs
, and ContainerDefaultArgs
are combined to form the final command that is run in the container. This depends greatly on if an ENTRYPOINT
is present in the base image. This property takes one of three values: "DefaultArgs"
, "Entrypoint"
, or "None"
.
Entrypoint
:- In this mode, the entrypoint is defined by
ContainerAppCommand
,ContainerAppCommandArgs
, andContainerDefaultArgs
.
- In this mode, the entrypoint is defined by
None
:- In this mode, the entrypoint is defined by
ContainerEntrypoint
,ContainerEntrypointArgs
, andContainerDefaultArgs
.
- In this mode, the entrypoint is defined by
DefaultArgs
:- This is the most complex mode—if none of the
ContainerEntrypoint[Args]
items are present, theContainerAppCommand[Args]
andContainerDefaultArgs
are used to create the entrypoint and command. The base image entrypoint for base images that have it hard-coded todotnet
or/usr/bin/dotnet
is skipped so that you have complete control. - If both
ContainerEntrypoint
andContainerAppCommand
are present, thenContainerEntrypoint
becomes the entrypoint, andContainerAppCommand
becomes the command.
- This is the most complex mode—if none of the
Note
The ContainerEntrypoint
and ContainerEntrypointArgs
configuration items have been deprecated as of .NET 8.
Important
This is for advanced users-most apps shouldn't need to customize their entrypoint to this degree. For more information and if you'd like to provide use cases for your scenarios, see GitHub: .NET SDK container builds discussions.
ContainerUser
The user configuration property controls the default user that the container runs as. This is often used to run the container as a non-root user, which is a best practice for security. There are a few constraints for this configuration to be aware of:
- It can take various forms—username, linux user ids, group name, linux group id,
username:groupname
, and other ID variants. - There's no verification that the user or group specified exists on the image.
- Changing the user can alter the behavior of the app, especially in regards to things like File System permissions.
The default value of this field varies by project TFM and target operating system:
- If you're targeting .NET 8 or higher and using the Microsoft runtime images, then:
- on Linux the rootless user
app
is used (though it's referenced by its user ID) - on Windows the rootless user
ContainerUser
is used
- on Linux the rootless user
- Otherwise, no default
ContainerUser
is used
<PropertyGroup>
<ContainerUser>my-existing-app-user</ContainerUser>
</PropertyGroup>
Tip
The APP_UID
environment variable is used to set user information in your container. This value can come from environment variables defined in your base image (like that Microsoft .NET images do), or you can set it yourself via the ContainerEnvironmentVariable
syntax.
To configure your app to run as a root user, set the ContainerUser
property to root
. In your project file, add the following:
<PropertyGroup>
<ContainerUser>root</ContainerUser>
</PropertyGroup>
Alternatively, you can set this value when calling dotnet publish
from the command line:
dotnet publish -p ContainerUser=root
Default container labels
Labels are often used to provide consistent metadata on container images. This package provides some default labels to encourage better maintainability of the generated images.
org.opencontainers.image.created
is set to the ISO 8601 format of the current UTCDateTime
.
For more information, see Implement conventional labels on top of existing label infrastructure.
Clean up resources
In this article, you published a .NET worker as a container image. If you want, delete this resource. Use the docker images
command to see a list of installed images.
docker images
Consider the following example output:
REPOSITORY TAG IMAGE ID CREATED SIZE
dotnet-worker-image 1.0.0 25aeb97a2e21 12 seconds ago 191MB
Tip
Image files can be large. Typically, you would remove temporary containers you created while testing and developing your app. You usually keep the base images with the runtime installed if you plan on building other images based on that runtime.
To delete the image, copy the image ID and run the docker image rm
command:
docker image rm 25aeb97a2e21