Container Images
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The C# Reference bibliography lists the reference material consulted while preparing these pages. |
The .NET SDK builds container images itself: dotnet publish -t:PublishContainer publishes the application, layers it
on a Microsoft base image, writes the image configuration and pushes the result — to the local Docker daemon, a
registry or a tar file — without a Dockerfile and without Docker being needed to build. Since .NET 10 this works for
console applications too, not only web projects. This page builds images of Ledgerline's report sample with .NET
SDK 10.0.402 and Docker 29.9. Running ASP.NET Core in containers is covered in
ASP.NET Core Deployment — Containers.
The first image
dotnet publish samples/Ledgerline.Report -t:PublishContainer
docker run --rm ledgerline-report:latest
Ledgerline.Report -> ~/ledgerline/samples/Ledgerline.Report/bin/Release/net10.0/Ledgerline.Report.dll
Ledgerline.Report -> ~/ledgerline/samples/Ledgerline.Report/bin/Release/net10.0/publish/
Building image 'ledgerline-report' with tags 'latest' on top of base image 'mcr.microsoft.com/dotnet/runtime:10.0'.
Pushed image 'ledgerline-report:latest' to local registry via 'docker'.
[
{
"Account": "1000-Cash",
"Debit": 500.00,
…
Everything was derived from the project: the repository name from the assembly name (lower-cased, . replaced by
-), the tag latest, and the base image from the target framework and the project type — runtime for a console
app, aspnet for a web app, runtime-deps for self-contained and Native AOT apps. The configuration it wrote:
docker inspect ledgerline-report:latest --format '{{json .Config.User}} {{json .Config.WorkingDir}} {{json .Config.Entrypoint}}'
"1654" "/app/" ["dotnet","/app/Ledgerline.Report.dll"]
The image runs as the non-root user app (UID 1654, the APP_UID of the Microsoft images) by default. Its labels
come from the package metadata in Directory.Build.props — org.opencontainers.image.authors, licenses, url,
version — plus the base image name and digest and org.opencontainers.image.created, the publish time.
The container properties
| Property | Purpose |
|---|---|
|
The full base image reference, overriding the inferred one. |
|
A suffix for the inferred tag: |
|
|
|
One tag, or several separated by |
|
The image name ( |
|
Push to this registry instead of the local daemon. |
|
The local tool to push to: |
|
The user to run as ( |
|
Write a tar file instead of pushing. |
Others set ports, environment variables, labels, the entry point and multi-architecture builds
(ContainerRuntimeIdentifiers).
Publishing to a registry
A throw-away registry:2 on localhost:5000 stands in for a real registry:
docker run -d --name reg -p 5000:5000 registry:2
dotnet publish samples/Ledgerline.Report -t:PublishContainer \
-p:ContainerRepository=ledgerline/report \
-p:ContainerImageTags='"1.0.0;latest"' \
-p:ContainerFamily=noble-chiseled \
-p:ContainerRegistry=localhost:5000
curl -s localhost:5000/v2/ledgerline/report/tags/list
Building image 'ledgerline/report' with tags '1.0.0,latest' on top of base image 'mcr.microsoft.com/dotnet/runtime:10.0-noble-chiseled-extra'.
Pushed image 'ledgerline/report:1.0.0, latest' to registry 'localhost:5000'.
{"name":"ledgerline/report","tags":["latest","1.0.0"]}
Three things to notice:
-
The inner quotes in
ContainerImageTags. A;on the command line separates MSBuild properties, so a list value must be quoted twice ('"1.0.0;latest"'in a POSIX shell). -
-extrawas added to the family. Chiseled images have no ICU; because the project does not setInvariantGlobalization, the SDK chose thenoble-chiseled-extravariant so that culture-aware formatting keeps working. The chiseled image was 197 MB uncompressed against 300 MB for the defaultruntime:10.0image, which since .NET 10 is Ubuntu 24.04 "noble" (Debian images are no longer shipped). -
No credentials were needed for
localhost. For a real registry the SDK reads the credentials thatdocker loginstored, so a CI job logs in to the registry before publishing.
Publishing to a tarball
Without a registry, or to scan an image before pushing it, write an archive:
dotnet publish samples/Ledgerline.Report -t:PublishContainer \
-p:ContainerArchiveOutputPath=pub/report.tar.gz -p:ContainerImageFormat=OCI
Building image 'ledgerline-report' with tags 'latest' on top of base image 'mcr.microsoft.com/dotnet/runtime:10.0'.
Pushed image 'ledgerline-report:latest' to local archive at '~/ledgerline/pub/report.tar.gz'.
Despite the .tar.gz name, file reported a plain POSIX tar archive (83 MB) in OCI layout (blobs/sha256/…): the
SDK does not compress the archive. docker load -i pub/report.tar.gz imports it.
Source: Publish .NET app to a tarball.
Reproducible images
Publishing the same commit twice in a row to the registry produced two different manifests:
Docker-Content-Digest: sha256:9a2ea36a5e696271666796bce1779769b9b2fb8cd0bc4d102c68391a9857a4a4
Docker-Content-Digest: sha256:a21db6498689bf4f69fd7786253eb2dd8ccb9943b952139d01365e0bed624f79
The layers were the same; the image configuration was not, because org.opencontainers.image.created and the
created fields hold the publish time. A digest that changes on every build defeats digest pinning and provenance
checks. The .NET 11 SDK previews use SOURCE_DATE_EPOCH for those timestamps, so independent publishes of the same
inputs produce the same digest
(Reproducible and
Deterministic Builds).
Dockerfile or SDK?
The SDK route needs no Docker to build, applies the non-root default and the labels, and keeps the image definition in
the project. A Dockerfile is still the choice when the image needs operating-system packages or extra files that are
not part of the publish output, or a multi-stage build that runs tests inside the image. Ledgerline's CI example on
CI Builds and Caching uses a Dockerfile
for that reason.
Common mistakes
-
An unquoted
;inContainerImageTags— MSBuild splits it into separate properties. -
Choosing
noble-chiseledand expecting no ICU — the SDK adds-extraunlessInvariantGlobalizationis set. -
Running as root out of habit — the default user is
app; write to/tmpor a mounted volume. -
Expecting identical digests from identical sources on .NET 10 — the creation time differs.
-
Pinning the base image by tag only — use
ContainerBaseImagewith a digest for repeatable builds.