.NET SDK Container Publishing: No Dockerfile Required

Starting with .NET 7, the SDK can publish your application directly as a container image — no Dockerfile needed. This feature is built into dotnet publish and produces optimised container images using the same base images you'd choose manually.

The Basics

Add the container publishing package to your project:

config.xml
<ItemGroup>
  <PackageReference Include="Microsoft.NET.Build.Containers" Version="9.0.0" />
</ItemGroup>

For .NET 8 and later with the Microsoft.NET.Sdk.Web SDK, this package is included automatically. Then publish:

terminal
dotnet publish --os linux --arch x64 /t:PublishContainer

That's it. The SDK builds your application, selects an appropriate base image, and creates a container image in your local Docker daemon.

Configuring the Image

Control the image name, tag, and registry through MSBuild properties in your .csproj:

config.xml
<PropertyGroup>
  <ContainerImageName>mycompany/myapi</ContainerImageName>
  <ContainerImageTag>1.0.0</ContainerImageTag>
  <ContainerRegistry>ghcr.io</ContainerRegistry>
</PropertyGroup>

You can also set multiple tags:

config.xml
<ItemGroup>
  <ContainerImageTags Include="1.0.0;latest" />
</ItemGroup>

Choosing the Base Image

By default, the SDK picks the appropriate mcr.microsoft.com/dotnet/aspnet or mcr.microsoft.com/dotnet/runtime image based on your project type. Override it for smaller images:

config.xml
<PropertyGroup>
  <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:9.0-alpine</ContainerBaseImage>
</PropertyGroup>

Or use chiselled images:

config.xml
<PropertyGroup>
  <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled</ContainerBaseImage>
</PropertyGroup>

For self-contained AOT applications, use runtime-deps:

config.xml
<PropertyGroup>
  <ContainerBaseImage>mcr.microsoft.com/dotnet/runtime-deps:9.0-noble-chiseled</ContainerBaseImage>
  <PublishAot>true</PublishAot>
</PropertyGroup>

Environment Variables and Ports

Configure runtime settings declaratively:

config.xml
<ItemGroup>
  <ContainerEnvironmentVariable Include="ASPNETCORE_URLS" Value="http://+:8080" />
  <ContainerEnvironmentVariable Include="DOTNET_EnableDiagnostics" Value="0" />
</ItemGroup>

<ItemGroup>
  <ContainerPort Include="8080" Type="tcp" />
</ItemGroup>

Running as Non-Root

Set the container user:

config.xml
<PropertyGroup>
  <ContainerUser>app</ContainerUser>
</PropertyGroup>

The official .NET base images include an app user. This is the recommended configuration for production.

Publishing to a Registry

Push directly to a container registry without Docker:

terminal
dotnet publish --os linux --arch x64 /t:PublishContainer \
    /p:ContainerRegistry=ghcr.io \
    /p:ContainerImageName=mycompany/myapi \
    /p:ContainerImageTag=1.0.0

The SDK pushes the image directly using the registry's HTTP API. You don't even need Docker installed — only the .NET SDK.

For authentication, the SDK reads credentials from Docker's config file (~/.docker/config.json) or environment variables:

terminal
export DOTNET_CONTAINER_REGISTRY_UNAME=myuser
export DOTNET_CONTAINER_REGISTRY_PWORD=mytoken

CI Pipeline Example

Here's a GitHub Actions workflow that builds and pushes a container image:

.github/workflows/container-publish.yml
name: Build and Push Container

on:
  push:
    tags: ['v*']

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '9.0.x'

      - name: Log in to GHCR
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

      - name: Extract version
        id: version
        run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT

      - name: Publish container
        run: |
          dotnet publish src/MyApi/MyApi.csproj \
            --os linux --arch x64 \
            /t:PublishContainer \
            /p:ContainerRegistry=ghcr.io \
            /p:ContainerImageName=${{ github.repository }} \
            /p:ContainerImageTag=${{ steps.version.outputs.VERSION }}

When to Use SDK Publishing vs Dockerfile

SDK container publishing is excellent when:

Stick with a Dockerfile when:

Comparison

Feature Dockerfile SDK Publishing
Custom OS packages Yes No
Docker required Yes No
Layer caching control Full Limited
Maintenance burden Higher Lower
Multi-stage builds Yes Not needed

Summary

SDK container publishing removes the Dockerfile from simple .NET applications. For APIs and console apps that don't need custom system packages, it's the fastest path from code to container. Configure everything in MSBuild properties, publish directly to a registry, and let the SDK handle the rest.