Start Debugging

Dependente de framework vs autocontido vs Native AOT para uma imagem de contêiner do .NET 11

Dependente de framework sobre uma imagem aspnet chiseled é o padrão certo para um serviço ASP.NET Core no .NET 11, porque a camada do runtime é compartilhada entre serviços e uma CVE do runtime é corrigida trocando a imagem base. Autocontido com trimming e Native AOT compram uma imagem de 2x a 5x menor e uma inicialização a frio bem mais rápida, e custam isso. Tamanhos publicados reais, a conta das camadas compartilhadas e o bug de inferência de imagem base do .NET 11 que quebra o caminho AOT.

Para um serviço ASP.NET Core comum e de longa duração no .NET 11, publique dependente de framework sobre uma imagem aspnet chiseled. É a menor coisa que você de fato envia (alguns poucos megabytes de aplicação em cima de uma camada de runtime que seus outros serviços já baixaram), e uma CVE do runtime é corrigida recompilando sobre uma nova tag de imagem base em vez de recompilar, testar de novo e implantar de novo a aplicação. Mude para autocontido com trimming quando a aplicação precisar fixar um patch específico do runtime ou rodar sobre uma imagem base sem nenhum .NET. Recorra ao Native AOT apenas quando a inicialização a frio ou a memória por pod for a restrição dominante e o dotnet publish não reportar nenhum aviso de AOT em toda a sua árvore de dependências. Os números de tamanho que as pessoas citam para AOT são reais, mas para uma frota eles medem a coisa errada: imagens dependentes de framework compartilham uma única camada de runtime entre todos os serviços de um nó, e as autocontidas e AOT não.

Tudo aqui tem como alvo <TargetFramework>net11.0</TargetFramework>. O .NET 11 está no Preview 7 (11.0.100-preview.7.26381.103, lançado em 2026-08-11) enquanto escrevo isto, com a versão final prevista para novembro de 2026. As tags de imagem da versão prévia carregam um qualificador -preview que a versão final remove, então 11.0-preview-resolute-chiseled hoje vira 11.0-resolute-chiseled em novembro. Os mecanismos abaixo estão estáveis desde o .NET 8, então quase tudo se aplica sem mudanças no .NET 9 e no .NET 10.

Os três modos como imagens de contêiner

PropriedadeDependente de frameworkAutocontido + trimmingNative AOT
Repositório da imagem basedotnet/aspnet ou dotnet/runtimedotnet/runtime-depsdotnet/runtime-deps
O runtime mora emna camada da imagem basena camada da sua aplicaçãocompilado dentro do binário
Camada de runtime compartilhada entre serviçosSimNãoNão
Uma CVE do runtime é corrigida combaixar uma nova tag base, recompilarnovo SDK, recompilar, testar, implantarnovo SDK, recompilar, testar, implantar
Avança para o patch instaladoSimNãoNão
Ativado pornada (é o padrão)--self-contained -p:PublishTrimmed=true-p:PublishAot=true
Precisa de um RIDNãoSimSim
A máquina de build precisa de toolchain CNãoNãoSim (clang, zlib1g-dev)
Reflexão, Reflection.Emit, carga de pluginsCompletaAvisos de trimming, falhas possíveis em execuçãoRestrita ou indisponível
Imagem de exemplo, comprimida52.81 MB21.86 MB11.60 MB

Esses três últimos números vêm do relatório de tamanho de imagens de contêiner do .NET em dotnet/dotnet-docker, medidos sobre o exemplo releasesapi com .NET 10.0 e imagens base noble-chiseled. Os detalhes completos daqui a pouco, porque essa linha é a que engana as pessoas.

O que cada modo realmente coloca na imagem

O tooling de contêineres do SDK infere a imagem base a partir do seu projeto, e a regra é curta. Conforme a referência de conteinerização, um projeto autocontido recebe mcr.microsoft.com/dotnet/runtime-deps, um projeto ASP.NET Core recebe mcr.microsoft.com/dotnet/aspnet, e qualquer outro recebe mcr.microsoft.com/dotnet/runtime. A tag é a parte numérica do seu TFM, com ContainerFamily acrescentado como sufixo.

Essa inferência é a história inteira:

Os três herdam o mesmo endurecimento das imagens da Microsoft: o usuário sem privilégios app com UID 1654, exposto por $APP_UID, e a porta 8080 em vez da 80, ambos introduzidos no .NET 8. Imagens chiseled ainda não trazem shell, nem gerenciador de pacotes, nem curl, então depuração com docker exec e health checks baseados em shell não funcionam em nenhum dos três modos se você escolher uma família chiseled.

Como publicar cada um dos três

Dependente de framework, sem precisar de RID, direto para uma base ASP.NET Core chiseled:

# .NET 11 SDK 11.0.100-preview.7. Framework-dependent onto aspnet:11.0-preview-resolute-chiseled.
dotnet publish --os linux --arch x64 /t:PublishContainer \
  -p ContainerFamily=resolute-chiseled \
  -p ContainerRepository=orders-api

Autocontido com trimming. PublishTrimmed implica SelfContained, mas escreva os dois para que quem ler no futuro não precise lembrar disso:

# .NET 11 SDK 11.0.100-preview.7. Self-contained + trimmed onto runtime-deps:11.0-preview-resolute-chiseled.
dotnet publish --os linux --arch x64 /t:PublishContainer \
  --self-contained \
  -p PublishTrimmed=true \
  -p ContainerFamily=resolute-chiseled \
  -p ContainerRepository=orders-api

Native AOT. PublishAot implica autocontido, e precisa do toolchain C da plataforma na máquina de build:

# .NET 11 SDK 11.0.100-preview.7. Native AOT onto runtime-deps:11.0-preview-resolute-chiseled.
# Requires clang and zlib1g-dev locally, or build inside sdk:11.0-preview-aot.
dotnet publish --os linux --arch x64 /t:PublishContainer \
  -p PublishAot=true \
  -p ContainerFamily=resolute-chiseled \
  -p ContainerRepository=orders-api

Se você prefere fazer isso a partir do CI sem instalar clang no agente, a imagem AOT do SDK é a razão de aquelas tags existirem:

# .NET 11 preview. Multi-stage AOT build.
FROM mcr.microsoft.com/dotnet/sdk:11.0-preview-resolute-aot AS build
WORKDIR /src
COPY . .
RUN dotnet publish OrdersApi/OrdersApi.csproj -c Release -r linux-x64 -p:PublishAot=true -o /app

FROM mcr.microsoft.com/dotnet/runtime-deps:11.0-preview-resolute-chiseled
WORKDIR /app
COPY --from=build /app/OrdersApi .
USER $APP_UID
ENTRYPOINT ["./OrdersApi"]

Para o conjunto completo de propriedades Container*, controle de tags e autenticação em registries, veja o passo a passo sobre publicar uma aplicação .NET 11 como imagem de contêiner sem Dockerfile.

Os números de tamanho publicados

A Microsoft publica tamanhos medidos para uma web API mínima de exemplo em cada variante de imagem base, então não há necessidade de especular. Estes são os tamanhos comprimidos do exemplo releasesapi no .NET 10.0:

Imagem baseDependente de frameworkAutocontido + trimmingNative AOT
Ubuntu completo (10.0)92.48 MB61.53 MB51.27 MB
10.0-noble-chiseled52.81 MB21.86 MB11.60 MB
10.0-noble-chiseled-extra67.68 MB36.82 MB26.56 MB
10.0-alpine51.93 MB20.95 MB10.69 MB
10.0-alpine-extra66.50 MB35.52 MB25.25 MB

Duas coisas saltam dessa tabela na hora. Primeiro, a família da imagem base é uma alavanca maior do que o modo de implantação. Mover uma aplicação dependente de framework da imagem Ubuntu completa para noble-chiseled economiza 39.67 MB, que é mais do que trocar essa mesma aplicação de dependente de framework para Native AOT na imagem completa economiza (41.21 MB) e não exige nenhum do trabalho de compatibilidade. Se você ainda não passou para chiseled, faça isso primeiro e meça de novo antes de considerar qualquer outra coisa.

Segundo, Native AOT sobre chiseled é mesmo cerca de 4.5x menor do que dependente de framework sobre chiseled. É um ganho genuíno, e para uma função scale-to-zero ou um nó de altíssima densidade é decisivo.

A conta das camadas compartilhadas que vira o argumento do tamanho

Aqui está a parte que o relatório de tamanho não consegue mostrar, porque ele mede uma imagem isolada.

Imagens de contêiner são camadas endereçadas por conteúdo. Se dez dos seus serviços fazem build FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview-resolute-chiseled, todo nó que os executa baixa e armazena aquela camada de runtime exatamente uma vez. O custo marginal do décimo primeiro serviço é a sua própria camada de aplicação, que para um serviço ASP.NET Core dependente de framework são alguns poucos megabytes de IL.

Faça a aritmética para dez serviços em um nó, usando a coluna chiseled acima:

Autocontido é o pior dos três em escala de frota mesmo ganhando de dependente de framework por 2.4x em uma imagem isolada, porque trimming é por aplicação e não consegue deduplicar entre aplicações. Native AOT é pequeno o bastante para continuar na frente, mas sua vantagem cai de 4.5x para bem menos de 2x. Armazenamento do registry, banda de download entre zonas e pressão de disco do nó seguem esse segundo cálculo, não o primeiro. Meça sua própria frota antes de migrar qualquer coisa por causa de tamanho.

Patching: quem corrige uma CVE do runtime

Este é o argumento que deveria de fato decidir para a maioria dos times, e é o que a visão geral de publicação diz sem rodeios. Uma aplicação dependente de framework “avança automaticamente para o último patch de segurança do .NET disponível no ambiente”, enquanto uma implantação autocontida “não avança” e “o runtime do .NET só pode ser atualizado publicando uma nova versão da aplicação”.

Em termos de contêiner:

Se a sua organização tem um controle de “corrigir CVEs críticas em N dias”, essa diferença não é uma nota de rodapé. É a razão para ficar em dependente de framework a menos que algo force a saída.

Globalização é a chave escondida entre chiseled e chiseled-extra

Imagens -chiseled, -alpine e as -distroless do Azure Linux vêm sem ICU e tzdata, então só funcionam com aplicações em modo de globalização invariante. As variantes -extra devolvem ICU, tzdata e libstdc++, que é de onde vêm aqueles 15 MB de diferença na tabela de tamanhos.

Para publicações autocontidas e AOT o SDK tenta ajudar: se InvariantGlobalization for false ele direciona você para uma variante -extra. Para publicações dependentes de framework você escolhe a família, então cabe a você deixar a propriedade coerente:

<!-- .NET 11, net11.0. Required if you target a plain -chiseled or -alpine base. -->
<PropertyGroup>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

Erre isso e o contêiner morre na inicialização com Couldn't find a valid ICU package installed on the system, que tem o próprio artigo de correção. E o modo invariante não é de graça: comparação de strings sensível a cultura, ToUpper e ToLower para caracteres não ASCII e consultas de TimeZoneInfo mudam de comportamento. Se você localiza qualquer coisa ou formata moeda, pague os 15 MB do -extra.

O problema do .NET 11: a inferência de imagem base ainda diz noble

O tooling de contêineres calcula o codinome do Ubuntu para a tag inferida a partir da versão do SDK, e nas versões prévias do .NET 11 essa consulta só conhece jammy (SDK abaixo de 8.0.300) e noble (8.0.300 em diante). Como 11.0.100 satisfaz a segunda condição, ela retorna noble, mas as imagens do .NET 11 no MCR são publicadas sob resolute (Ubuntu 26.04). O resultado, reportado como dotnet/sdk#53553:

error CONTAINER1015: Unable to access the repository 'dotnet/runtime-deps' at tag '11.0.0-preview.2-noble-chiseled-extra'

O raio de impacto é exatamente o dos caminhos de que este artigo trata. Publicação dependente de framework passa ilesa, porque não entra no ramo de inferência de codinome. Publicações autocontidas com trimming e com PublishAot=true batem nele. A correção é parar de depender da inferência e nomear a família explicitamente, que é por isso que todos os comandos acima a passam:

# .NET 11 SDK 11.0.100-preview.7. Explicit family, no codename inference.
dotnet publish --os linux --arch x64 /t:PublishContainer \
  -p PublishAot=true \
  -p ContainerFamily=resolute-chiseled

Definir ContainerBaseImage com um nome totalmente qualificado também funciona e ignora ContainerFamily por completo. Fixar a família explicitamente é boa prática de qualquer jeito: é o que impede um SDK futuro de mover sua frota silenciosamente para outra distribuição. A rotação de tags do Ubuntu 26.04 é a mesma lição pelo lado do .NET 10.

A restrição que decide por você

A maioria dos times nunca chega a pesar tamanhos, porque uma restrição dura decide:

Recomendação, repetida

Por padrão, dependente de framework sobre aspnet:11.0-<family>-chiseled. É a imagem mais barata em escala de frota, é o único modo em que uma CVE do runtime é uma troca de imagem base em vez de um release, e é o único que envia um artefato único agnóstico de RID. Vá para Native AOT sobre runtime-deps:11.0-<family>-chiseled quando a inicialização a frio ou a densidade de memória for a restrição que manda e sua árvore de dependências publicar limpa. Use autocontido com trimming como opção intermediária quando precisar fixar a versão do runtime ou uma imagem base sem .NET, entendendo que ele é o pior dos três para o armazenamento de toda a frota. Escolha o que escolher, defina ContainerFamily explicitamente, e passe a imagem para chiseled antes de otimizar qualquer outra coisa.

Relacionado

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar