Set SOURCE_DATE_EPOCH to the same Unix timestamp on two .NET 11 container builds and you can reproduce the image manifest digest.
Both jobs must also use the same source, SDK, packages, platform, and base image.
You can compare their digests locally before pushing either image to a registry.
Why Would Two Builds of the Same Commit Differ?
Your staging tests pass. The production job checks out the same commit, rebuilds the image, and tags it with the same commit SHA. A tag with the same spelling can still point to a different image in production.
The manifest lists the digests of the image configuration and its layers. If the second job writes a different timestamp into a layer's archive header, the layer digest changes even when the application files are identical. That changes the manifest digest too.
The .NET 11 SDK writes the application files into the layer in a consistent order. For reproducible container builds, give both jobs the same timestamp.
Build Two Images Without Pushing Them
You'll need a Unix shell, the .NET 11 SDK, tar, and jq.
Create the service with dotnet new web -n StockApi -f net11.0 --no-https and run cd StockApi.
The SDK downloads the base image. Save each archive one directory above the project so the next build can't include the previous archive as application content.
publish_image() {
dotnet publish -c Release -r linux-arm64 --no-self-contained \
-t:PublishContainer \
-p:ContainerRepository=stock-api \
-p:ContainerImageFormat=OCI \
-p:SOURCE_DATE_EPOCH=1794268800 \
-p:ContainerArchiveOutputPath="../$1.tar.gz"
}
publish_image first
dotnet clean -c Release -r linux-arm64
publish_image second
Both publishes target Linux Arm64 and use the .NET 11 SDK selected on the build machine. Keep the architecture fixed even if the build agents run on different machines. The SDK builds the image without a Dockerfile, and writing to an archive lets you inspect it without registry credentials.
Compare the Manifest Digests
These are single-platform OCI archives, so the first entry in each OCI image index identifies its Linux Arm64 manifest.
Extract that digest from index.json in both archives.
image_digest() {
tar -xOf "../$1.tar.gz" index.json | jq -er '.manifests[0].digest'
}
first_digest=$(image_digest first) &&
second_digest=$(image_digest second) &&
test "$first_digest" = "$second_digest" &&
echo "Same image manifest"
On SDK 11.0.100 the comparison printed Same image manifest.
Now change the response string in Program.cs from Hello World! to Inventory ready. and run publish_image changed.
The first 12 digits are enough to see which digests changed in this run. Keep the full digests in the CI comparison, as the shell command does.
| Build | Manifest digest prefix | Application layer digest prefix |
|---|---|---|
| First publish | fb520e11f1b6 | 548110c07118 |
| Clean build of the same source | fb520e11f1b6 | 548110c07118 |
| One response string changed | 87f16fdc2583 | f89bb34acb60 |
To check the application layer yourself, open the manifest referenced by index.json and print its last layer digest.
manifest=$(image_digest changed)
tar -xOf ../changed.tar.gz "blobs/sha256/${manifest#sha256:}" |
jq -r '.layers[-1].digest'
Which Inputs Should CI Pin?
The hard-coded timestamp lets you run the example outside a Git checkout.
In CI, -p:SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)" uses the commit timestamp, so a retry tomorrow gets the same value.
The web template defaults to a base tag such as mcr.microsoft.com/dotnet/aspnet:11.0.
That tag can point to a different image after the next servicing update.
To reproduce an earlier release, set the container base image with -p:ContainerBaseImage=mcr.microsoft.com/dotnet/aspnet@sha256:... and supply the full base digest from that build.
Update the pin when you adopt a patched base image.
Pin the SDK version and package versions too. If a custom build task writes the current time into a generated file, the publisher still packages those bytes.
When two digests differ, compare the base layers first, then the application layer. If every layer matches, compare the image configuration. Its digest is also part of the manifest.
Summary
Deploy the digest staging already tested when you can. For a separate production build, have the deployment job compare its manifest digest with the one that passed staging and stop if they differ.
Frequently Asked Questions
How do I produce a reproducible image with the .NET 11 SDK?
Publish with the PublishContainer target and supply a stable SOURCE_DATE_EPOCH, expressed as Unix seconds. Keep the application, SDK, packages, platform, and base image fixed. Compare the resulting image manifest digests.
Where is the image digest in a .NET SDK container archive?
For a single-platform OCI archive, read index.json and inspect the digest of its manifest descriptor. That identifies the platform image manifest, which references the configuration and layer blobs.
Does setting SOURCE_DATE_EPOCH pin the container base image?
No. A mutable tag can point to a newer base on a subsequent build. Set ContainerBaseImage to an image reference with a digest to freeze that input, and update it deliberately when picking up base image fixes.

