{"id":51406,"date":"2024-04-15T10:05:00","date_gmt":"2024-04-15T17:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=51406"},"modified":"2024-05-13T12:47:39","modified_gmt":"2024-05-13T19:47:39","slug":"streamline-container-build-dotnet-8","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/streamline-container-build-dotnet-8\/","title":{"rendered":"Streamline your container build and publish with .NET 8"},"content":{"rendered":"<p>.NET 8 is a big step forward for building and using containers, with improvements for performance, security, and usability. We&#8217;ve been working over several releases to make .NET one of the simplest and most secure container platforms, as a default experience. Those efforts have all come together in an integrated way with .NET 8. We&#8217;ve delivered on the most common requests: <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/securing-containers-with-rootless\/\">non-root images<\/a>, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-dotnet-chiseled-containers\/\">smaller image size<\/a>, and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/docker\/publish-as-container\">built-in image publishing<\/a>. We&#8217;ve also delivered some critical features that are required for advanced workflows.<\/p>\n<p>We&#8217;ve pivoted to <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-builtin-container-support-for-the-dotnet-sdk\/\"><code>dotnet publish<\/code><\/a> as our recommended approach to container publishing. It makes it really easy to produce images. That&#8217;s all driven by <a href=\"https:\/\/github.com\/dotnet\/msbuild\/\">MSBuild<\/a> (which powers <code>dotnet publish<\/code>). It can infer intent and make decisions on your behalf, for example, on the best base image to use. As we add and expand features, like <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/deploying\/native-aot\/\">Native AOT<\/a>, we teach <code>dotnet publish<\/code> the best default container publishing choices for that scenario. That makes container publishing a straightforward extension of your development process.<\/p>\n<p>Dockerfiles remain very popular and we continue to provide <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/README.md\">extensive samples<\/a> for them, including how to <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/e5e8164460037e77902cd269c788eccbdeea5edd\/samples\/dotnetapp\/Dockerfile#L20\">enable new scenarios like non-root<\/a>. In fact, we often reach for Dockerfiles when we&#8217;re prototyping ideas or reproducing a customer issue.<\/p>\n<p>We recently worked with the team at Docker to improve <a href=\"https:\/\/docs.docker.com\/language\/dotnet\/containerize\/\">the <code>docker<\/code> CLI .NET developer experience<\/a>. It is now possible to run <code>docker init<\/code> in a .NET project directory to generate a working Dockerfile. Those Dockerfiles use some <a href=\"https:\/\/docs.docker.com\/build\/guide\/mounts\/#add-a-cache-mount\">very useful caching features<\/a> that we&#8217;re starting to adopt in our samples.<\/p>\n<p>We recommending taking a look at the <a href=\"https:\/\/github.com\/richlander\/container-workshop\">.NET 8 Container Workshop<\/a> that we shared in our <a href=\"https:\/\/www.youtube.com\/watch?v=scIAwLrruMY\">.NET Conf 2023 container talk<\/a> as a great way to learn all the new .NET 8 capabilities.<\/p>\n<p>In this post, you will learn how to:<\/p>\n<ul>\n<li>Publish a container image using dotnet publish<\/li>\n<li>Produce an image for a specific distro with the SDK<\/li>\n<li>Build a globalization-friendly chiseled image<\/li>\n<li>Inspect a container image to understand what is included in the chiseled image<\/li>\n<li>Understand important differences between SDK published container images and Dockerfile based images<\/li>\n<\/ul>\n<p>The post will demonstrate some quick demos (with useful syntax that you can re-use). We&#8217;ll go into more detail in follow-up posts. If you are less into commandline experience, you&#8217;ll find that <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/debugging-dotnet-containers-with-visual-studio-code-docker-tools\/\">Visual Studio Code works much the same way<\/a>.<\/p>\n<h2>Publish a &#8220;Hello world&#8221; container image<\/h2>\n<p>Let&#8217;s start with the most basic experience with the console app template.<\/p>\n<pre><code class=\"language-bash\">$ dotnet new console -o myapp\n$ cd myapp\n$ dotnet publish -t:PublishContainer -p:EnableSdkContainerSupport=true\n  Building image 'myapp' with tags 'latest' on top of base image 'mcr.microsoft.com\/dotnet\/runtime:8.0'.\n  Pushed image 'myapp:latest' to local registry via 'docker'.\n$ docker run --rm myapp\nHello, World!<\/code><\/pre>\n<p>The <code>PublishContainer<\/code> task produces a container image that it pushes to the local Docker daemon. It knows that a <code>runtime<\/code> base image should be used for console apps and that an <code>8.0<\/code> tag should be used for a .NET 8 app. The resulting image can be run with <code>docker run<\/code>, which requires <a href=\"https:\/\/www.docker.com\/products\/docker-desktop\/\">Docker Desktop<\/a> (or equivalent) to be installed.<\/p>\n<p>The <code>EnableSdkContainerSupport<\/code> property is required for publishing console projects as container images. It is not set by default for console apps (or any other app that uses the <code>Microsoft.NET.Sdk<\/code> SDK), while ASP.NET Core apps have it set (implicitly). This property can be included in a project file, which is recommended, or set via the CLI (as demonstrated above).<\/p>\n<h2>Inspecting the image<\/h2>\n<p>It&#8217;s often easier to understand what is going on with a better demo.<\/p>\n<p>The following is a two line <code>Program.cs<\/code>, which will print runtime information.<\/p>\n<pre><code class=\"language-csharp\">using System.Runtime.InteropServices;\nConsole.WriteLine($\"Hello {Environment.UserName}, using {RuntimeInformation.OSDescription} on {RuntimeInformation.OSArchitecture}\");<\/code><\/pre>\n<p>The project file has been updated to include the <code>EnableSdkContainerSupport<\/code> property so it doesn&#8217;t need to be provided with the <code>dotnet publish<\/code> command.<\/p>\n<pre><code class=\"language-xml\">&lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt;\n\n  &lt;PropertyGroup&gt;\n    &lt;OutputType&gt;Exe&lt;\/OutputType&gt;\n    &lt;TargetFramework&gt;net8.0&lt;\/TargetFramework&gt;\n    &lt;RootNamespace&gt;hello_dotnet&lt;\/RootNamespace&gt;\n    &lt;ImplicitUsings&gt;enable&lt;\/ImplicitUsings&gt;\n    &lt;Nullable&gt;enable&lt;\/Nullable&gt;\n    &lt;!-- This property is needed for `Microsoft.NET.Sdk` projects--&gt;\n    &lt;EnableSdkContainerSupport&gt;true&lt;\/EnableSdkContainerSupport&gt;\n  &lt;\/PropertyGroup&gt;\n\n&lt;\/Project&gt;<\/code><\/pre>\n<p>Let&#8217;s rebuild and re-run the container image again.<\/p>\n<pre><code class=\"language-bash\">$ dotnet publish -t:PublishContainer\n  Building image 'myapp' with tags 'latest' on top of base image 'mcr.microsoft.com\/dotnet\/runtime:8.0'.\n  Pushed image 'myapp:latest' to local registry via 'docker'.\n$ docker run --rm myapp\nHello app, using Debian GNU\/Linux 12 (bookworm) on X64<\/code><\/pre>\n<p>.NET images use Debian by default, which is apparent from the output. We can also see that <code>app<\/code> is the user running the process. We&#8217;ll discuss the <code>app<\/code> user in much more depth in a following post.<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm --entrypoint bash myapp -c \"cat \/etc\/os-release | head -n 1\"\nPRETTY_NAME=\"Debian GNU\/Linux 12 (bookworm)\"<\/code><\/pre>\n<p>The image is indeed Debian, as demonstrated by the <code>os-release<\/code> file in the image. In fact, the <code>RuntimeInformation<\/code> API gets the data from this same file, which is why the strings match.<\/p>\n<h2>Producing an image for a specific distro<\/h2>\n<p>It is possible to build an image for a specific distro with the SDK. We&#8217;ll start with publishing the <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/tree\/main\/samples\/aspnetapp\/aspnetapp\">aspnetapp<\/a> for Alpine.<\/p>\n<pre><code class=\"language-bash\">$ dotnet publish --os linux-musl -t:PublishContainer\n  Building image 'aspnetapp' with tags 'latest' on top of base image 'mcr.microsoft.com\/dotnet\/aspnet:8.0-alpine'.\n  Pushed image 'aspnetapp:latest' to local registry via 'docker'.\n$ docker run --rm -it -p 8000:8080 aspnetapp\ninfo: Microsoft.Hosting.Lifetime[14]\n      Now listening on: http:\/\/[::]:8080\ninfo: Microsoft.Hosting.Lifetime[0]\n      Application started. Press Ctrl+C to shut down.  <\/code><\/pre>\n<p>The SDK knows when a build targets <code>linux-musl<\/code> that an Alpine image should be used. This feature is new with .NET SDK 8.0.200. Previously, <code>ContainerFamily=alpine<\/code> had to be used to get the same result.<\/p>\n<p>The following image shows the web app in the browser.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2024\/05\/aspnetapp-alpine.png\" alt=\"aspnetapp sample in browser, using an Alpine image\" \/><\/p>\n<p><code>ContainerFamily=jammy<\/code> is needed to produce Ubuntu images and <code>ContainerFamily=jammy-chiseled<\/code> is needed to produce Ubuntu Chiseled containers.<\/p>\n<pre><code class=\"language-bash\">$ dotnet publish -t:PublishContainer -p:ContainerFamily=jammy-chiseled\n  aspnetapp -&gt; \/home\/rich\/git\/dotnet-docker\/samples\/aspnetapp\/aspnetapp\/bin\/Release\/net8.0\/aspnetapp.dll\n  aspnetapp -&gt; \/home\/rich\/git\/dotnet-docker\/samples\/aspnetapp\/aspnetapp\/bin\/Release\/net8.0\/publish\/\n  Building image 'aspnetapp' with tags 'latest' on top of base image 'mcr.microsoft.com\/dotnet\/aspnet:8.0-jammy-chiseled'.\n  Pushed image 'aspnetapp:latest' to local registry via 'docker'.<\/code><\/pre>\n<p>You can see that an Ubuntu Chiseled image is used. You can also see that <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/whats-new\/dotnet-8\/sdk#dotnet-publish-and-dotnet-pack-assets\"><code>publish<\/code> defaults to <code>Release<\/code> with .NET 8<\/a>.<\/p>\n<p>The Alpine support is arguably simpler. Alpine is the only distro we support for the <a href=\"https:\/\/musl.libc.org\/\"><code>linux-musl<\/code> configuration<\/a>, while we produce both Debian and Ubuntu container images for <code>linux<\/code> with Debian being the default (which we do not intend to change).<\/p>\n<h2>World-ready chiseled images<\/h2>\n<p>We heard a lot of excitement about chiseled images when we first announced them, however, some users told us that they needed <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/issues\/5014\">globalization-friendly chiseled images<\/a>. We produced those as <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/discussions\/5092\"><code>extra<\/code><\/a> images that include <code>icu<\/code> and <code>tzdata<\/code> libraries.<\/p>\n<p>Let&#8217;s take a look at how that works with the <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/globalapp\/README.md\"><code>globalapp<\/code> sample<\/a>, starting with <code>ContainerFamily=jammy-chiseled<\/code>.<\/p>\n<pre><code class=\"language-bash\">$ dotnet publish -t:PublishContainer -p:EnableSdkContainerSupport=true -p:ContainerFamily=jammy-chiseled\n  Building image 'globalapp' with tags 'latest' on top of base image 'mcr.microsoft.com\/dotnet\/runtime:8.0-jammy-chiseled'.\n  Pushed image 'globalapp:latest' to local registry via 'docker'.\n$ docker run --rm globalapp\nHello, World!\n\n****Print baseline timezones**\nUtc: (UTC) Coordinated Universal Time; 04\/01\/2024 23:41:20\nLocal: (UTC) Coordinated Universal Time; 04\/01\/2024 23:41:20\n\n****Print specific timezone**\nUnhandled exception. System.TimeZoneNotFoundException: The time zone ID 'America\/Los_Angeles' was not found on the local computer.\n ---&gt; System.IO.DirectoryNotFoundException: Could not find a part of the path '\/usr\/share\/zoneinfo\/America\/Los_Angeles'.\n   at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirError)\n   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String path, OpenFlags flags, Int32 mode, Boolean failForSymlink, Boolean&amp; wasSymlink, Func`4 createOpenException)\n   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, UnixFileMode openPermissions, Int64&amp; fileLength, UnixFileMode&amp; filePermissions, Boolean failForSymlink, Boolean&amp; wasSymlink, Func`4 createOpenException)\n   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)\n   at System.TimeZoneInfo.ReadAllBytesFromSeekableNonZeroSizeFile(String path, Int32 maxFileSize)\n   at System.TimeZoneInfo.TryGetTimeZoneFromLocalMachineCore(String id, TimeZoneInfo&amp; value, Exception&amp; e)\n   --- End of inner exception stack trace ---\n   at System.TimeZoneInfo.FindSystemTimeZoneById(String id)\n   at Program.&lt;Main&gt;$(String[] args) in \/home\/rich\/git\/dotnet-docker\/samples\/globalapp\/Program.cs:line 24<\/code><\/pre>\n<p>That&#8217;s surely not good! This app doesn&#8217;t work correctly without <code>tzdata<\/code> and that&#8217;s exactly what the error is telling us. That&#8217;s also the problem that motivated the feature requests we received that led to the <code>extra<\/code> images. Note that the exception is due to <code>tzdata<\/code>, but we&#8217;d see an ICU related exception as well if the program had gotten farther.<\/p>\n<p>Let&#8217;s try again with the <code>extra<\/code> image. I&#8217;ll also pass in a specific timezone this time using the <code>TZ<\/code> environment variable.<\/p>\n<pre><code class=\"language-bash\">$ dotnet publish -t:PublishContainer -p:EnableSdkContainerSupport=true -p:ContainerFamily=jammy-chiseled-extra\n  Building image 'globalapp' with tags 'latest' on top of base image 'mcr.microsoft.com\/dotnet\/runtime:8.0-jammy-chiseled-extra'.\n  Pushed image 'globalapp:latest' to local registry via 'docker'.\n$ docker run --rm -e TZ=\"Pacific\/Auckland\" globalapp\nHello, World!\n\n****Print baseline timezones**\nUtc: (UTC) Coordinated Universal Time; 04\/02\/2024 00:48:41\nLocal: (UTC+12:00) New Zealand Time; 04\/02\/2024 13:48:41\n\n****Print specific timezone**\nHome timezone: America\/Los_Angeles\nDateTime at home: 04\/01\/2024 16:44:02\n\n****Culture-specific dates**\nCurrent: 04\/01\/2024\nEnglish (United States) -- en-US:\n4\/1\/2024 11:44:02 PM\n4\/1\/2024\n11:44 PM\nEnglish (Canada) -- en-CA:\n2024-04-01 11:44:02 p.m.\n2024-04-01\n11:44 p.m.\nFrench (Canada) -- fr-CA:\n2024-04-01 23 h 44 min 02 s\n2024-04-01\n23 h 44\nCroatian (Croatia) -- hr-HR:\n01. 04. 2024. 23:44:02\n01. 04. 2024.\n23:44\njp (Japan) -- jp-JP:\n4\/1\/2024 23:44:02\n4\/1\/2024\n23:44\nKorean (South Korea) -- ko-KR:\n2024. 4. 1. \uc624\ud6c4 11:44:02\n2024. 4. 1.\n\uc624\ud6c4 11:44\nPortuguese (Brazil) -- pt-BR:\n01\/04\/2024 23:44:02\n01\/04\/2024\n23:44\nChinese (China) -- zh-CN:\n2024\/4\/1 23:44:02\n2024\/4\/1\n23:44\n\n****Culture-specific currency:**\nCurrent: \u00a41,337.00\nen-US: $1,337.00\nen-CA: $1,337.00\nfr-CA: 1 337,00 $\nhr-HR: 1.337,00 kn\njp-JP: \u00a5 1337\nko-KR: \u20a91,337\npt-BR: R$ 1.337,00\nzh-CN: \u00a51,337.00\n\n****Japanese calendar**\n08\/18\/2019\n01\/08\/18\n\u5e73\u6210\u5143\u5e748\u670818\u65e5\n\u5e73\u6210\u5143\u5e748\u670818\u65e5\n\n****String comparison**\nComparison results: `0` mean equal, `-1` is less than and `1` is greater\nTest: compare i to (Turkish) \u0130; first test should be equal and second not\n0\n-1\nTest: compare \u00c5 A\u030a; should be equal\n0<\/code><\/pre>\n<p>That&#8217;s a lot better. Let&#8217;s do a size comparison to get the point across.<\/p>\n<pre><code class=\"language-bash\">$ dotnet publish -t:PublishContainer -p:EnableSdkContainerSupport=true\n  Building image 'globalapp' with tags 'latest' on top of base image 'mcr.microsoft.com\/dotnet\/runtime:8.0'.\n  Pushed image 'globalapp:latest' to local registry via 'docker'.\n$ dotnet publish -t:PublishContainer -p:EnableSdkContainerSupport=true -p:ContainerFamily=jammy-chiseled-extra -p:ContainerRepository=globalapp-jammy-chiseled-extra\n  Building image 'globalapp-jammy-chiseled-extra' with tags 'latest' on top of base image 'mcr.microsoft.com\/dotnet\/runtime:8.0-jammy-chiseled-extra'.\n  Pushed image 'globalapp-jammy-chiseled-extra:latest' to local registry via 'docker'.\n$ docker images globalapp\nREPOSITORY   TAG       IMAGE ID       CREATED          SIZE\nglobalapp    latest    91422e1c0b0e   17 seconds ago   193MB\n$ docker images globalapp-jammy-chiseled-extra\nREPOSITORY                       TAG       IMAGE ID       CREATED          SIZE\nglobalapp-jammy-chiseled-extra   latest    da9621e19cad   59 seconds ago   123MB<\/code><\/pre>\n<p>These commands build the sample for the default Debian image then for the Ubuntu Chiseled <code>extra<\/code> image. The difference is 70MB (uncompressed). That&#8217;s a pretty nice win. You could similarly use Alpine with the <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/dotnetapp\/Dockerfile.alpine-icu\">following pattern<\/a>.<\/p>\n<h2>Frequently asked questions<\/h2>\n<blockquote>\n<p>How does this all work?<\/p>\n<\/blockquote>\n<p>The mechanism that <code>PublishContainer<\/code> uses is more straightforward than one might guess. Container images are compressed files, composed of <a href=\"https:\/\/github.com\/richlander\/container-registry-api\">layers of compressed files<\/a>. The <code>PublishContainer<\/code> MSBuild Target builds the app, compresses it in the correct format (with metadata), downloads a base image (also a compressed file) from a registry, and then packages the layers together in (again) the correct compressed format. Much of this is accomplished with the (relatively new) <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.formats.tar\"><code>TarFile<\/code> class<\/a>. In fact, all of this container functionality was implemented only after <code>TarFile<\/code> was added.<\/p>\n<blockquote>\n<p>&#8220;What about up to date checks?&#8221;<\/p>\n<\/blockquote>\n<p>Many users build images with <code>docker build --pull<\/code>. That&#8217;s a good idea to ensure that a new application image uses a fresh base image (for example with CVE fixes). <code>dotnet publish -t:PublishContainer<\/code> does the same thing by default. The image manifest for the specified tag (like <code>mcr.microsoft.com\/dotnet\/aspnet:8.0<\/code>) is always pulled. If the associated digest doesn&#8217;t exist in the local cache, then the image is pulled.<\/p>\n<p><code>dotnet publish<\/code> uses its own cache for images. If you build an application image, it will pull a base image to build on top of. This base image will not affect the images that are maintained by Docker Desktop, for example. The <code>dotnet publish<\/code> cache is maintained in temporary storage, at the location specified by <code>System.IO.Path.GetTempPath()<\/code> on a given machine.<\/p>\n<blockquote>\n<p>&#8220;Where&#8217;s the Dockerfile?&#8221;<\/p>\n<\/blockquote>\n<p>We sometimes get asked if users can see the <code>Dockerfile<\/code> we are using or if it can be modified. There is no <code>Dockerfile<\/code> that is used in this scenario. <code>docker build<\/code> and a <code>Dockerfile<\/code> assumes a Linux operating system, in particular to execute <code>RUN<\/code> commands (like to run <code>apt<\/code>, <code>curl<\/code> or <code>tar<\/code>). We don&#8217;t have or support anything like that. <code>PublishContainer<\/code> is solely downloading base image layers and then copying one container layer onto another and packaging them up as an <a href=\"https:\/\/github.com\/opencontainers\/image-spec\">OCI image<\/a>.<\/p>\n<blockquote>\n<p>&#8220;This is a great &#8216;no Docker&#8217; solution!&#8221;<\/p>\n<\/blockquote>\n<p>That&#8217;s not the intent and not really reality. The <code>PublishContainer<\/code> support can be thought of as a &#8220;no Dockerfile&#8221; solution, however <a href=\"https:\/\/www.docker.com\/\">Docker<\/a> is incredibly useful, and you can see that the post relies on it extensively. We also see users using <a href=\"https:\/\/podman.io\/\">podman<\/a>. If you are really adventurous, you can use <a href=\"https:\/\/containerd.io\/\">containerd<\/a>, directly.<\/p>\n<p>There is one way in which this is correct. <code>dotnet publish<\/code> supports <a href=\"https:\/\/github.com\/richlander\/container-workshop\/blob\/main\/push-to-registry.md\">pushing images to a registry<\/a>. It supports several. This capability can be very useful for GitHub Actions and similar CI environments. We&#8217;ll cover that in a later post.<\/p>\n<blockquote>\n<p>&#8220;How do I install packages with <code>apt<\/code> if there is no <code>Dockerfile<\/code>?&#8221;<\/p>\n<\/blockquote>\n<p>You don&#8217;t, directly. <code>dotnet publish<\/code> can download any base image. You can make your own base image, for example, with additional packages installed, push that to a registry and then <a href=\"https:\/\/github.com\/richlander\/container-workshop\/blob\/main\/publish-oci-reference.md\">reference that<\/a>. It can be based on one of the Microsoft images or not. <code>dotnet publish<\/code> is happy to download it.<\/p>\n<h2>Summary<\/h2>\n<p>Containers have become synonymous with the cloud for many styles of workloads. While we&#8217;ve been saying that for years, it is more true now than ever. Delivering first-class container support has been a focus of the team for several years now, and we remain committed to that.<\/p>\n<p>With .NET 8, we&#8217;ve delivered: <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/securing-containers-with-rootless\/\">non-root images<\/a> (for security and usability), <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-dotnet-chiseled-containers\/\">chiseled images<\/a> (for performance and security),  and <a href=\"https:\/\/learn.microsoft.com\/shows\/containers-with-dotnet-and-docker-for-beginners\/\">container image building with <code>dotnet publish<\/code><\/a> (for usability).<\/p>\n<p>There is still a lot more to tell with what we delivered in .NET 8, which we&#8217;ll describe and demonstrate in future posts.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>.NET 8 is a big step forward for building and using containers, with improvements for performance, security, and usability. Let&#8217;s take a look at some enhancements to the .NET CLI for building and publishing containers.<\/p>\n","protected":false},"author":1312,"featured_media":51823,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7689,7237],"tags":[7828,7174,57,7829,7827],"class_list":["post-51406","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-cloud-native","category-containers","tag-net-sdk","tag-cli","tag-containers","tag-dotnet-cli","tag-sdk"],"acf":[],"blog_post_summary":"<p>.NET 8 is a big step forward for building and using containers, with improvements for performance, security, and usability. Let&#8217;s take a look at some enhancements to the .NET CLI for building and publishing containers.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/51406","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/1312"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=51406"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/51406\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/51823"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=51406"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=51406"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=51406"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}