Containers and .NET
What a container image is, why it ended "works on my machine", and when to use the .NET SDK image versus the runtime image.
docker run --rm -p 8080:8080 mcr.microsoft.com/dotnet/aspnet:10.0A container image is a packaged copy of everything a program needs in order to run: your compiled app, the .NET runtime underneath it, and a thin slice of Linux under that. A container is one running instance of an image. The image is the blueprint and the container is the house built from it.
Before images, a program worked on the machine it was built on and then failed on the server, because the server had a different runtime version or a missing library. An image closes that gap. The same bytes run in both places.
The official .NET images
Microsoft publishes them under mcr.microsoft.com/dotnet. The tag is the .NET version, so 10.0 for this course.
| Repository | What is inside | Use it for |
|---|---|---|
dotnet/sdk | Compiler, NuGet client, full runtime | Building, never shipping |
dotnet/aspnet | Runtime plus the ASP.NET Core libraries | The final image of a web app |
dotnet/runtime | Runtime only | A console app or worker |
dotnet/runtime-deps | Native dependencies, no .NET | Self-contained or native AOT apps |
Which image belongs where
The SDK image carries a compiler and is many times the size of the runtime image. It belongs in a build stage only. The last stage of your build should be aspnet or runtime.
What trips people up
Shipping the SDK image to production. It works, which is why it survives review, and it hands anyone who gets into that container a compiler, a package manager and your source.
Example
// Program.cs - the app that will go inside the image.
// Every official .NET image sets DOTNET_RUNNING_IN_CONTAINER=true, so an
// app can tell where it is running. It is set here in code as well, so the
// printed output is the same on your own machine as it is in a container.
Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", "true");
string? flag = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER");
bool inContainer = flag == "true";
Console.WriteLine("app: inventory-api");
Console.WriteLine($"in container: {(inContainer ? "yes" : "no")}");
// Output:
// app: inventory-api
// in container: yesWhen to use it
- Give a new team member one command that runs the service, with no .NET install on their machine.
- Pin the exact runtime version a service uses, so a server upgrade cannot change it underneath you.
- Run two services that need different .NET versions on the same host without conflict.
More examples
Two images, two jobs
The whole reason there are two .NET image families. The SDK image exists so a build can happen somewhere reproducible; the aspnet image exists so the shipped artifact contains no compiler. Reach for this split the first time you write a Dockerfile, which is the next lesson.
# The sdk image can compile. This stage is thrown away afterwards.
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# The aspnet image cannot compile. It only runs what you copy into it,
# and it is the image that actually ships.
FROM mcr.microsoft.com/dotnet/aspnet:10.0Pulling and listing images
Run this once before you write any Dockerfile. Seeing the two sizes side by side is what makes the build-stage/runtime-stage split feel necessary rather than ceremonial.
# Download both images from the Microsoft container registry.
docker pull mcr.microsoft.com/dotnet/sdk:10.0
docker pull mcr.microsoft.com/dotnet/aspnet:10.0
# List them with their sizes. The sdk row is far larger than the aspnet row,
# because it contains the compiler, MSBuild and the NuGet client.
docker images | grep dotnetThe shorthand for create plus start
docker run is create plus start, and with --rm it is create plus start plus rm. They are the same thing. You will see both forms: scripts that need the container id before it starts use the long form, and everything else uses run.
# Longhand: make a container from the image, then start it, then remove it.
docker create --name probe mcr.microsoft.com/dotnet/runtime:10.0 dotnet --info
docker start --attach probe
docker rm probe
# Shorthand: the same three steps in one command. --rm deletes the
# container when the process exits, so nothing is left behind.
docker run --rm mcr.microsoft.com/dotnet/runtime:10.0 dotnet --info
Discussion