Start Debugging

Dependiente del framework vs autocontenido vs Native AOT para una imagen de contenedor de .NET 11

Dependiente del framework sobre una imagen aspnet chiseled es el valor por defecto correcto para un servicio ASP.NET Core en .NET 11, porque la capa del runtime se comparte entre servicios y una CVE del runtime se corrige actualizando la imagen base. Autocontenido con trimming y Native AOT compran una imagen de 2x a 5x más pequeña y un arranque en frío mucho más rápido, y te cuestan eso. Tamaños publicados reales, las cuentas de las capas compartidas y el bug de inferencia de imagen base de .NET 11 que rompe la ruta AOT.

Para un servicio ASP.NET Core normal y de larga duración en .NET 11, publica dependiente del framework sobre una imagen aspnet chiseled. Es lo más pequeño que realmente envías (unos pocos megabytes de aplicación encima de una capa de runtime que tus otros servicios ya descargaron), y una CVE del runtime se corrige recompilando sobre una nueva etiqueta de imagen base en lugar de recompilar, volver a probar y volver a implementar la aplicación. Cambia a autocontenido con trimming cuando la aplicación deba fijar un parche concreto del runtime o ejecutarse sobre una imagen base sin .NET alguno. Recurre a Native AOT solo cuando el arranque en frío o la memoria por pod sea la restricción dominante y dotnet publish no reporte ninguna advertencia AOT en todo tu árbol de dependencias. Las cifras de tamaño que la gente cita para AOT son reales, pero para una flota miden lo que no toca: las imágenes dependientes del framework comparten una sola capa de runtime entre todos los servicios de un nodo, y las autocontenidas y AOT no.

Todo lo de aquí apunta a <TargetFramework>net11.0</TargetFramework>. .NET 11 está en Preview 7 (11.0.100-preview.7.26381.103, publicada el 2026-08-11) mientras escribo esto, con la versión final prevista para noviembre de 2026. Las etiquetas de imagen de la versión preliminar llevan un calificador -preview que la versión final elimina, así que 11.0-preview-resolute-chiseled hoy se convierte en 11.0-resolute-chiseled en noviembre. Los mecanismos de abajo son estables desde .NET 8, así que casi todo aplica sin cambios en .NET 9 y .NET 10.

Los tres modos como imágenes de contenedor

PropiedadDependiente del frameworkAutocontenido + trimmingNative AOT
Repositorio de imagen basedotnet/aspnet o dotnet/runtimedotnet/runtime-depsdotnet/runtime-deps
El runtime vive enla capa de la imagen basela capa de tu aplicacióncompilado dentro del binario
Capa de runtime compartida entre serviciosNoNo
Una CVE del runtime se corrige condescargar una nueva etiqueta base, recompilarnuevo SDK, recompilar, volver a probar, volver a implementarnuevo SDK, recompilar, volver a probar, volver a implementar
Avanza al parche instaladoNoNo
Se activa connada (es el valor por defecto)--self-contained -p:PublishTrimmed=true-p:PublishAot=true
Necesita un RIDNo
La máquina de compilación necesita toolchain de CNoNoSí (clang, zlib1g-dev)
Reflexión, Reflection.Emit, carga de pluginsCompletaAdvertencias de trimming, posibles fallos en ejecuciónRestringida o no disponible
Imagen de ejemplo, comprimida52.81 MB21.86 MB11.60 MB

Esas tres últimas cifras vienen del informe de tamaño de imágenes de contenedor de .NET en dotnet/dotnet-docker, medidas sobre el ejemplo releasesapi con .NET 10.0 e imágenes base noble-chiseled. Los detalles completos en un momento, porque esa fila es la que confunde a la gente.

Qué pone realmente cada modo en la imagen

El tooling de contenedores del SDK infiere la imagen base a partir de tu proyecto, y la regla es corta. Según la referencia de contenerización, un proyecto autocontenido recibe mcr.microsoft.com/dotnet/runtime-deps, un proyecto ASP.NET Core recibe mcr.microsoft.com/dotnet/aspnet, y cualquier otro recibe mcr.microsoft.com/dotnet/runtime. La etiqueta es la parte numérica de tu TFM, con ContainerFamily añadido como sufijo.

Esa inferencia es toda la historia:

Los tres heredan el mismo endurecimiento de las imágenes de Microsoft: el usuario sin privilegios app con UID 1654, expuesto mediante $APP_UID, y el puerto 8080 en vez del 80, ambos introducidos en .NET 8. Las imágenes chiseled además no traen shell, ni gestor de paquetes, ni curl, así que la depuración con docker exec y los health checks basados en shell no funcionan en ninguno de los tres modos si eliges una familia chiseled.

Cómo publicar cada uno de los tres

Dependiente del framework, sin necesidad de RID, directo a una 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

Autocontenido con trimming. PublishTrimmed implica SelfContained, pero escribe ambos para que quien lo lea en el futuro no tenga que recordarlo:

# .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 autocontenido, y necesita el toolchain de C de la plataforma en la máquina de compilación:

# .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

Si prefieres hacer esto desde CI sin instalar clang en el agente, la imagen AOT del SDK es la razón por la que existen esas etiquetas:

# .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 el conjunto completo de propiedades Container*, el control de etiquetas y la autenticación contra registros, consulta el recorrido sobre publicar una aplicación .NET 11 como imagen de contenedor sin Dockerfile.

Las cifras de tamaño publicadas

Microsoft publica tamaños medidos para una API web mínima de ejemplo en cada variante de imagen base, así que no hace falta especular. Estos son los tamaños comprimidos del ejemplo releasesapi en .NET 10.0:

Imagen baseDependiente del frameworkAutocontenido + 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

De esa tabla salen dos cosas de inmediato. Primero, la familia de imagen base es una palanca más grande que el modo de despliegue. Mover una aplicación dependiente del framework de la imagen Ubuntu completa a noble-chiseled ahorra 39.67 MB, que es más de lo que ahorra cambiar esa misma aplicación de dependiente del framework a Native AOT sobre la imagen completa (41.21 MB) y no requiere nada del trabajo de compatibilidad. Si todavía no has pasado a chiseled, hazlo primero y vuelve a medir antes de considerar cualquier otra cosa.

Segundo, Native AOT sobre chiseled sí es unas 4.5 veces más pequeño que dependiente del framework sobre chiseled. Es una ganancia real, y para una función scale-to-zero o un nodo de muy alta densidad es decisiva.

Las cuentas de capas compartidas que dan la vuelta al argumento del tamaño

Aquí está la parte que el informe de tamaños no puede mostrarte, porque mide una imagen de forma aislada.

Las imágenes de contenedor son capas direccionadas por contenido. Si diez de tus servicios compilan todos FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview-resolute-chiseled, cada nodo que los ejecuta descarga y almacena esa capa de runtime exactamente una vez. El coste marginal del undécimo servicio es su propia capa de aplicación, que para un servicio ASP.NET Core dependiente del framework son unos pocos megabytes de IL.

Haz la aritmética para diez servicios en un nodo, usando la columna chiseled de arriba:

Autocontenido es el peor de los tres a escala de flota aunque gane a dependiente del framework por 2.4x en una imagen aislada, porque el trimming es por aplicación y no puede deduplicar entre aplicaciones. Native AOT es lo bastante pequeño como para seguir por delante, pero su ventaja baja de 4.5x a bastante menos de 2x. El almacenamiento del registro, el ancho de banda de descarga entre zonas y la presión de disco del nodo siguen este segundo cálculo, no el primero. Mide tu propia flota antes de migrar nada por motivos de tamaño.

Parcheo: quién corrige una CVE del runtime

Este es el argumento que debería decidirlo de verdad para la mayoría de los equipos, y es el que la visión general de publicación expone sin rodeos. Una aplicación dependiente del framework “avanza automáticamente al último parche de seguridad de .NET disponible en el entorno”, mientras que una implementación autocontenida “no avanza” y “el runtime de .NET solo puede actualizarse publicando una nueva versión de la aplicación”.

En términos de contenedores:

Si tu organización tiene un control de “parchear CVEs críticas en N días”, esa diferencia no es una nota al pie. Es la razón para quedarse en dependiente del framework salvo que algo te obligue a salir.

La globalización es el interruptor oculto entre chiseled y chiseled-extra

Las imágenes -chiseled, -alpine y las -distroless de Azure Linux vienen sin ICU ni tzdata, así que solo funcionan con aplicaciones en modo de globalización invariante. Las variantes -extra devuelven ICU, tzdata y libstdc++, que es de donde salen esos 15 MB de diferencia de la tabla de tamaños.

Para las publicaciones autocontenidas y AOT el SDK intenta ayudar: si InvariantGlobalization es false te dirige a una variante -extra. Para las publicaciones dependientes del framework eliges la familia tú mismo, así que te toca a ti poner la propiedad acorde:

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

Si te equivocas aquí, el contenedor muere al arrancar con Couldn't find a valid ICU package installed on the system, que tiene su propio artículo de solución. Y el modo invariante no es gratis: la comparación de cadenas sensible a la cultura, ToUpper y ToLower para caracteres no ASCII y las búsquedas de TimeZoneInfo cambian de comportamiento. Si localizas algo o formateas moneda, paga los 15 MB de -extra.

El problema de .NET 11: la inferencia de imagen base sigue diciendo noble

El tooling de contenedores calcula el nombre en clave de Ubuntu para la etiqueta inferida a partir de la versión del SDK, y a día de hoy en las versiones preliminares de .NET 11 esa búsqueda solo conoce jammy (SDK por debajo de 8.0.300) y noble (8.0.300 en adelante). Como 11.0.100 cumple la segunda condición devuelve noble, pero las imágenes de .NET 11 en MCR se publican bajo resolute (Ubuntu 26.04). El 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'

El radio de impacto son exactamente las rutas de las que trata este artículo. La publicación dependiente del framework va bien, porque no pasa por la rama de inferencia del nombre en clave. Las publicaciones autocontenidas con trimming y las de PublishAot=true lo sufren las dos. La solución es dejar de depender de la inferencia y nombrar la familia de forma explícita, que es por lo que todos los comandos de arriba la pasan:

# .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

Poner ContainerBaseImage con un nombre totalmente cualificado también funciona y salta ContainerFamily por completo. Fijar la familia de forma explícita es buena práctica en cualquier caso: es lo que impide que un SDK futuro mueva tu flota a otra distribución en silencio. La rotación de etiquetas de Ubuntu 26.04 es la misma lección desde el lado de .NET 10.

La restricción que decide por ti

La mayoría de los equipos nunca llega a sopesar tamaños, porque una restricción dura lo decide:

Recomendación, repetida

Por defecto, dependiente del framework sobre aspnet:11.0-<family>-chiseled. Es la imagen más barata a escala de flota, es el único modo en el que una CVE del runtime es una actualización de imagen base en vez de una release, y es el único que envía un solo artefacto agnóstico al RID. Pasa a Native AOT sobre runtime-deps:11.0-<family>-chiseled cuando el arranque en frío o la densidad de memoria sea la restricción que manda y tu árbol de dependencias publique limpio. Usa autocontenido con trimming como opción intermedia cuando necesites fijar la versión del runtime o una imagen base sin .NET, entendiendo que es el peor de los tres para el almacenamiento de toda la flota. Elijas lo que elijas, define ContainerFamily de forma explícita, y pasa la imagen a chiseled antes de optimizar cualquier otra cosa.

Relacionado

Fuentes

Comments

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

< Volver