Skip to main content
SEMastery
DevOpsbeginner

Containerize Your .NET Applications Without a Dockerfile

Learn how to build container images for your .NET apps using the SDK and dotnet publish, with no Dockerfile needed. Beginner-friendly guide for .NET 10.

12 min readUpdated January 9, 2026

Cooking without writing the recipe card

Imagine your mother makes the same lunchbox every morning: rice, dal, a little sabzi, and a spoon of pickle. She does not write down a recipe card each day. She already knows the steps by heart, so she just packs the box and hands it to you.

Now imagine a second kitchen where, before making anything, the cook must first write a full recipe card listing every single step: "boil water, add rice, wait twelve minutes..." That works, but it is extra writing every time.

A Dockerfile is like that recipe card. It is a text file where you write every step to package your app into a container. It is powerful, but for many normal .NET apps it is more writing than you need.

The good news: the .NET SDK already knows how to pack a standard .NET app into a container. Just like your mother already knows the lunchbox by heart. So you can skip the recipe card and say, "SDK, please pack my app into a container." That is what this guide is about.

By the end, you will be able to turn any .NET app into a real container image with one command and a few lines in your project file — no Dockerfile at all.

What is a container, in one minute?

A container is a small, sealed box that holds your app and everything it needs to run: the .NET runtime, your code, and a tiny slice of an operating system. Because the box carries its own bits, it runs the same way on your laptop, on your friend's laptop, and on a big server in the cloud.

An image is the recipe-in-a-box that is saved on disk. A container is what you get when you actually run that image. One image, many containers — like one cake recipe that you can bake many times.

Figure 1: An image is the saved package. A running copy of it is a container. The same image runs the same way everywhere.

For years, the normal way to build that image was to write a Dockerfile. Now the SDK can build it for you.

The old way vs the new way

Here is the same job done two ways. Both produce a real image. One asks you to write and maintain a file; the other does not.

QuestionDockerfile waySDK way (no Dockerfile)
Do I write a separate file?Yes, a DockerfileNo, just your .csproj
How do I build?docker builddotnet publish /t:PublishContainer
Do I need Docker installed?YesOnly if loading locally
Who picks the base image?YouThe SDK (you can override)
Can I run apt-get install?YesNo
Good for typical web APIs?YesYes, and simpler

Neither way is "better" for every case. The SDK way is wonderful for normal apps. The Dockerfile way wins when you need to run custom shell steps. We will come back to that near the end.

The one command you need

Let us start with the smallest possible example. Make a tiny web API:

// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
app.MapGet("/", () => "Hello from a container, with no Dockerfile!");
 
app.Run();

Now, from the project folder, run this single command:

// This is a shell command, not C#. Run it in your terminal:
// dotnet publish --os linux --arch x64 /t:PublishContainer
//
// On .NET 10 you can often shorten it to:
// dotnet publish /t:PublishContainer

That is it. The SDK reads your project, picks a small Microsoft base image, copies your published app inside, and builds a proper container image. If you have Docker running, it loads the image into your local Docker so you can run it:

// Again, shell commands:
// docker run -p 8080:8080 your-app-name
// Then open http://localhost:8080 in your browser.

No Dockerfile was written. The SDK did the packing.

From code to running container, no Dockerfile

Write code
dotnet publish
Image built
Run container

Steps

1

Write code

Normal .NET app

2

dotnet publish

Use /t:PublishContainer

3

Image built

SDK packs it

4

Run container

docker run or deploy

The SDK does the middle steps that a Dockerfile used to do.

What happens behind the scenes

When you run that publish command, the SDK does a few clear steps for you. It is helpful to picture them so the "magic" feels less magical.

Figure 2: The steps the SDK runs when it builds your container image.

Notice the last step has three choices. This is the part many people miss, so let us slow down on it.

Three places your image can go

The SDK can send the finished image to three different destinations. You choose with simple settings.

  1. Local Docker daemon — the default. Needs Docker or Podman running. Great for testing on your own machine.
  2. A remote registry — like Docker Hub, GitHub Container Registry, or Azure Container Registry. Set ContainerRegistry (and your repository). No Docker needed for this — the SDK talks to the registry directly.
  3. A tarball file — a single .tar.gz on disk. Set ContainerArchiveOutputPath. No Docker needed either. You can move this file around and load it later.

Choosing a destination

Local
Registry
Tarball

Steps

1

Local

Test on your PC, needs Docker

2

Registry

Deploy, no Docker needed

3

Tarball

Save a file, no Docker needed

Pick one based on what you are doing right now.

This is why people say you can go "fully Docker-free." If you push to a registry or save a tarball in your CI pipeline, the build machine never needs a container runtime installed at all.

Configuring the image in your .csproj

All the settings live as MSBuild properties inside a <PropertyGroup> in your project file. No new syntax to learn — it is the same place you already set things like TargetFramework.

Here is a friendly, well-labelled example:

<!-- MyApi.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
 
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
 
    <!-- Image name (the "repository") -->
    <ContainerRepository>shop/orders-api</ContainerRepository>
 
    <!-- One or more tags, separated by semicolons -->
    <ContainerImageTags>1.4.0;latest</ContainerImageTags>
 
    <!-- Where to push. Leave out to use local Docker. -->
    <ContainerRegistry>ghcr.io</ContainerRegistry>
 
    <!-- Optional: choose your own base image -->
    <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:10.0</ContainerBaseImage>
 
    <!-- Optional: which port the app listens on -->
    <ContainerPort Include="8080" Type="tcp" />
  </PropertyGroup>
 
</Project>

(That snippet is XML, but our code box just needs it shown clearly.) Each line maps to one idea. Let us put the most common settings in a table so you can scan them quickly.

PropertyWhat it doesIf you skip it
ContainerRepositorySets the image nameUses your assembly name
ContainerImageTagOne tag, e.g. 1.4.0Uses latest
ContainerImageTagsSeveral tags by ;Uses latest
ContainerRegistryRemote registry to push toLoads to local Docker
ContainerBaseImageBase image to build onSDK picks a good default
ContainerArchiveOutputPathSave as a .tar.gz fileNot saved as a file

A small but important rule: image names and tags can only use lowercase letters, numbers, dots, dashes, and underscores, and must start with a letter or number. So Orders_API is fine as a repository segment only if lowercase: orders_api. If your assembly name has odd characters, set ContainerRepository yourself.

Setting things from the command line

You do not have to put everything in the .csproj. You can pass properties on the command line with /p:. This is handy in CI pipelines where the version number changes every build.

// Shell command examples.
// Bash:
//   dotnet publish /t:PublishContainer \
//     /p:ContainerRepository=shop/orders-api \
//     /p:ContainerImageTags='"1.4.0;latest"'
//
// PowerShell (note the backtick escaping for the semicolon):
//   dotnet publish /t:PublishContainer `
//     /p:ContainerRepository=shop/orders-api `
//     /p:ContainerImageTags="1.4.0`;latest"

The semicolon needs careful quoting because the shell and MSBuild both treat ; specially. Get the quoting right and you can ship several tags at once, such as a precise version and a moving latest.

A real CI/CD flow

Most teams build images inside a CI pipeline like GitHub Actions. The lovely part: because you can push straight to a registry, the runner does not need Docker installed. It just needs the .NET SDK.

Figure 3: A simple GitHub Actions flow that builds and pushes an image with no Dockerfile and no Docker daemon.

A trimmed pipeline step might look like this in your head: log in to the registry, then run dotnet publish /t:PublishContainer with the registry and tags set. No docker build, no docker push, no Dockerfile in the repo. Fewer moving parts means fewer things to break.

Multi-architecture images

Computers come in two common CPU shapes today: x64 (most laptops and servers) and ARM64 (newer Macs, Raspberry Pi, many cloud machines). An image built for one shape will not run on the other.

On .NET 10 you can build one image name that works for both by setting ContainerRuntimeIdentifiers:

<!-- In your .csproj -->
<PropertyGroup>
  <ContainerRuntimeIdentifiers>linux-x64;linux-arm64</ContainerRuntimeIdentifiers>
</PropertyGroup>

Now a single tag, say shop/orders-api:1.4.0, carries both builds. When a machine pulls it, it automatically gets the right one. This used to need extra Docker buildx tricks; now it is one line.

Making the image safer and smaller

Two quick wins many beginners enjoy.

Run as a non-root user. Running as root inside a container is risky. The SDK lets you set the user with ContainerUser. Modern Microsoft base images even include a ready-made non-root app user, so you can write <ContainerUser>app</ContainerUser> and feel safer instantly.

Pick a smaller base image. Smaller images pull faster and have fewer parts that could have security bugs. You can point ContainerBaseImage at a slimmer runtime, such as a chiseled or distroless variant from Microsoft. Less stuff inside means a smaller box.

<!-- A slim, non-root setup -->
<PropertyGroup>
  <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled</ContainerBaseImage>
  <ContainerUser>app</ContainerUser>
</PropertyGroup>

These are optional, but they are good habits to build early.

When you should still write a Dockerfile

The SDK approach is not magic for every situation. Be honest about its limits. It cannot run shell commands during the build. So if you need any of these, reach for a Dockerfile:

  • Installing extra OS packages, like apt-get install for a native library.
  • Building non-.NET parts in the same image, such as compiling a front-end bundle with npm.
  • Multi-stage builds that copy artifacts between several build stages.
  • Workflows that depend on docker compose build.

For a plain ASP.NET Core API, a worker service, or a console app, the SDK way is usually all you need and is simpler to maintain. Pick the tool that fits the job.

Figure 4: A quick decision guide for choosing the SDK way or a Dockerfile.

A note on the wider .NET world

A quick, honest heads-up while we are talking tooling. Some popular libraries that you might pair with a containerized app have changed their licensing. MediatR and MassTransit are now commercially licensed in their recent versions. That does not affect container publishing at all, but if you were planning to add them, check the license and pricing first so there are no surprises for your team.

Quick recap

  • A Dockerfile is like a recipe card. The .NET SDK already knows the recipe, so you can skip the card.
  • Build an image with one command: dotnet publish /t:PublishContainer. No Dockerfile needed.
  • Configure everything with MSBuild properties in your .csproj, like ContainerRepository, ContainerImageTags, and ContainerRegistry.
  • The image can go to local Docker, a remote registry, or a tarball file. Pushing to a registry or saving a tarball needs no Docker at all.
  • Use ContainerRuntimeIdentifiers on .NET 10 to ship one image that runs on both x64 and ARM64.
  • Make it safer and smaller with ContainerUser and a slim ContainerBaseImage.
  • Still write a Dockerfile when you need shell steps like apt-get install, non-.NET build stages, or docker compose build.

References and further reading

Related Posts