martes, 11 de febrero de 2020
Como sabemos, para ejecutar cualquier tipo de aplicación .NET Core en un equipo necesitamos tener instalado el runtime o el SDK de la plataforma. Esto es algo que podemos hacer muy fácilmente, simplemente acudiendo a la página oficial de descargas, eligiendo nuestro sistema operativo y siguiendo las instrucciones de instalación.
El hecho de que en el equipo destino esté preinstalado el runtime es muy interesante, entre otras cosas porque permite asegurar de antemano que en él se encontrarán todas las dependencias (frameworks, bibliotecas, paquetes, metapaquetes) necesarios para una correcta ejecución. Por tanto, para distribuir nuestra aplicación sólo debemos generar lo relativo a nuestro código, el resto ya estará allí.
Y todo esto está muy bien, pero, ¿qué pasa si quiero crear una aplicación portable, de forma que pueda distribuirla y ejecutarla sin necesidad de que el equipo destino tenga nada preinstalado?
Pues eso es lo que veremos en este post ;)
Para publicar en modo self contained desde Visual Studio, sólo tendremos que acudir al perfil de publicación y editar sus settings de la siguiente forma:
Como se observa en la captura anterior, en el Deployment mode debemos seleccionar "Self-contained", mientras que en la opción Target Runtime tendremos que indicar el runtime sobre el que vamos a ejecutar la aplicación. Este paso es importante, pues hará que los binarios a incluir en el paquete distribuible sean los específicos para el entorno indicado.
Esto también podemos hacerlo si queremos generar el paquete de publicación desde la línea de comandos, por ejemplo como sigue:
Pero aún podemos mejorarlo un poco...
Desde .NET Core 3.0, el SDK incluye de serie una funcionalidad que permite eliminar de los archivos distribuibles los paquetes que no sean utilizados por nuestra aplicación o sus dependencias. Para activarlo, basta con añadir el elemento
Un último detalle: como hemos comentado anteriormente, el trimming eliminará los paquetes que detecte que no son utilizados, pero puede haber ocasiones en las que nos interesa que este mecanismo no elimine algún ensamblado en particular. Por ejemplo, si nuestra aplicación utiliza reflexión o cualquier otro mecanismo para cargar o utilizar ensamblados, el trimmer no los detectará y asumirá que no se están usando, lo que provocará errores en tiempo de ejecución.
Para indicar que un ensamblado debe ser incluido obligatoriamente en el paquete de publicación, podemos usar el elemento
Podemos publicar como archivo único mediante una orden de la CLI como la siguiente:
Publicado en Variable not found.
El hecho de que en el equipo destino esté preinstalado el runtime es muy interesante, entre otras cosas porque permite asegurar de antemano que en él se encontrarán todas las dependencias (frameworks, bibliotecas, paquetes, metapaquetes) necesarios para una correcta ejecución. Por tanto, para distribuir nuestra aplicación sólo debemos generar lo relativo a nuestro código, el resto ya estará allí.
Esta forma de publicar aplicaciones se denomina framework-dependent, pues dependen de que determinados componentes del framework estén instalado en el destino.Por ejemplo, el paquete de publicación de una aplicación de consola prácticamente vacía, que únicamente muestra el mensaje "Hello world!", ocuparía solo 175K:
D:\MyConsoleApp\output>dir
El volumen de la unidad D es Datos
El número de serie del volumen es: 8CBC-81E3
Directorio de D:\MyConsoleApp\output
09/02/2020 18:47 <DIR> .
09/02/2020 18:47 <DIR> ..
09/02/2020 18:46 428 MyConsoleApp.deps.json
09/02/2020 18:46 4.608 MyConsoleApp.dll
09/02/2020 18:46 169.984 MyConsoleApp.exe
09/02/2020 18:46 668 MyConsoleApp.pdb
09/02/2020 18:46 154 MyConsoleApp.runtimeconfig.json
5 archivos 175.842 bytes
2 dirs 463.058.874.368 bytes libres
D:\MyConsoleApp\output>_
Otra ventaja de este tipo de distribución es que es cross platform pura, es decir, podemos copiar los archivos a cualquiera de los sistemas operativos soportados y, siempre que dispongan del runtime, nuestra aplicación podrá correr sobre ellos sin problema.Y todo esto está muy bien, pero, ¿qué pasa si quiero crear una aplicación portable, de forma que pueda distribuirla y ejecutarla sin necesidad de que el equipo destino tenga nada preinstalado?
Pues eso es lo que veremos en este post ;)
Publicación self-contained
Este tipo de publicación incluye todo lo necesario para que la aplicación funcione, sin necesidad de que el equipo destino disponga de componentes preinstalados. Es la opción más segura si desconocemos qué runtimes están instalados en el servidor, o incluso si queremos aislarnos de cambios o actualizaciones que pudieran instalarse en el servidor y que, de alguna forma, pudieran salpicarnos en el futuro.Para publicar en modo self contained desde Visual Studio, sólo tendremos que acudir al perfil de publicación y editar sus settings de la siguiente forma:
Como se observa en la captura anterior, en el Deployment mode debemos seleccionar "Self-contained", mientras que en la opción Target Runtime tendremos que indicar el runtime sobre el que vamos a ejecutar la aplicación. Este paso es importante, pues hará que los binarios a incluir en el paquete distribuible sean los específicos para el entorno indicado.
Esto también podemos hacerlo si queremos generar el paquete de publicación desde la línea de comandos, por ejemplo como sigue:
D:\MyConsoleApp>dotnet publish --self-contained -r win-x64 -c release -o output
Microsoft (R) Build Engine versión 16.4.0+e901037fe para .NET Core
Copyright (C) Microsoft Corporation. Todos los derechos reservados.
Restauración realizada en 138,79 ms para D:\MyConsoleApp\MyConsoleApp.csproj.
MyConsoleApp -> D:\MyConsoleApp\bin\release\netcoreapp3.1\win-x64\MyConsoleApp.dll
MyConsoleApp -> D:\MyConsoleApp\output\
D:\MyConsoleApp\output>_
En cualquier caso, el principal inconveniente de este modelo de distribución es que los archivos a mover son bastantes más, por lo que los despliegues serán más lentos y ocuparán mucho más espacio en disco. Por ejemplo, si hacemos un dir en la carpeta de resultados de la publicación veremos que hemos pasado de tener cinco archivos a más de doscientos, y el tamaño de menos de 200Kb a 69Mb:D:\MyConsoleApp\output>dir
El volumen de la unidad D es Datos
El número de serie del volumen es: 8CBC-81E3
Directorio de D:\MyConsoleApp\output
09/02/2020 18:56 <DIR> .
09/02/2020 18:56 <DIR> ..
20/04/2018 06:28 19.208 api-ms-win-core-console-l1-1-0.dll
20/04/2018 06:28 18.696 api-ms-win-core-datetime-l1-1-0.dll
20/04/2018 06:28 18.696 api-ms-win-core-debug-l1-1-0.dll
20/04/2018 06:28 18.696 api-ms-win-core-errorhandling-l1-1-0.dll
20/04/2018 06:29 22.280 api-ms-win-core-file-l1-1-0.dll
20/04/2018 06:37 18.696 api-ms-win-core-file-l1-2-0.dll
(... Omitidos más de 200 archivos ...)
09/12/2019 03:39 14.928 System.Xml.XmlDocument.dll
09/12/2019 03:40 16.768 System.Xml.XmlSerializer.dll
09/12/2019 03:40 14.200 System.Xml.XPath.dll
09/12/2019 03:39 15.952 System.Xml.XPath.XDocument.dll
20/04/2018 06:37 1.016.584 ucrtbase.dll
09/12/2019 03:40 15.440 WindowsBase.dll
225 archivos 69.122.606 bytes
2 dirs 462.918.475.776 bytes libres
D:\MyConsoleApp\output>_
Sí, pensaréis que es un exceso para mostrar un simple "Hola mundo", pero si tenemos en cuenta que estos archivos incluyen el framework y todas sus bibliotecas no sale tan mal parada la cosa. Lo importante en este caso es que podemos copiar esta carpeta a cualquier servidor Windows x64 y funcionará correctamente, sin necesidad de tener nada preinstalado.Pero aún podemos mejorarlo un poco...
Eliminando peso: Trimming de paquetes
Cuando hemos echado un ojo al contenido de la carpeta de destino de la publicación, vimos archivos comoSystem.Xml.XmlSerializer.dll
o System.Xml.XPath.dll
... ¿realmente necesitamos desplegar estos ensamblados en nuestra aplicación, que sólo muestra un "Hello world!"? Seguro que no.Desde .NET Core 3.0, el SDK incluye de serie una funcionalidad que permite eliminar de los archivos distribuibles los paquetes que no sean utilizados por nuestra aplicación o sus dependencias. Para activarlo, basta con añadir el elemento
<PublishTrimmed>
en el archivo .csproj
del proyecto:<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
Hecho esto, si volvemos a repetir la operación de publicación, veremos que el paquete de publicación se reducirá bastante:D:\MyConsoleApp>dotnet publish --self-contained -r win-x64 -c release -o output
Microsoft (R) Build Engine versión 16.4.0+e901037fe para .NET Core
Copyright (C) Microsoft Corporation. Todos los derechos reservados.
Restauración realizada en 26,72 ms para D:\MyConsoleApp\MyConsoleApp.csproj.
MyConsoleApp -> D:\MyConsoleApp\bin\Release\netcoreapp3.1\win-x64\MyConsoleApp.dll
Se está optimizando el tamaño de los ensamblados, lo que puede cambiar el
comportamiento de la aplicación. Asegúrese de probarlo después de publicar.
Consulte https://aka.ms/dotnet-illink
MyConsoleApp -> D:\MyConsoleApp\output\
D:\MyConsoleApp>dir output
El volumen de la unidad D es Datos
El número de serie del volumen es: 8CBC-81E3
Directorio de D:\MyConsoleApp\output
09/02/2020 19:05 <DIR> .
09/02/2020 19:05 <DIR> ..
20/04/2018 06:28 19.208 api-ms-win-core-console-l1-1-0.dll
20/04/2018 06:28 18.696 api-ms-win-core-datetime-l1-1-0.dll
20/04/2018 06:28 18.696 api-ms-win-core-debug-l1-1-0.dll
20/04/2018 06:28 18.696 api-ms-win-core-errorhandling-l1-1-0.dll
(... Omitidos más de 50 archivos ...)
09/02/2020 19:05 62.464 System.Console.dll
07/12/2019 16:40 9.555.840 System.Private.CoreLib.dll
09/02/2020 19:05 74.752 System.Runtime.Extensions.dll
20/04/2018 06:37 1.016.584 ucrtbase.dll
63 archivos 26.499.046 bytes
2 dirs 462.951.739.392 bytes libres
D:\MyConsoleApp>_
Mucho mejor ahora: hemos pasado de 225 archivos a 63, y reducido el peso de 69 a 26Mb. Aunque sigue siendo demasiado para un simple "Hello world!", al menos sabemos que es el mínimo al que podemos aspirar si queremos que el paquete distribuible de nuestra aplicación incluya el framework sobre el que será ejecutada.Un último detalle: como hemos comentado anteriormente, el trimming eliminará los paquetes que detecte que no son utilizados, pero puede haber ocasiones en las que nos interesa que este mecanismo no elimine algún ensamblado en particular. Por ejemplo, si nuestra aplicación utiliza reflexión o cualquier otro mecanismo para cargar o utilizar ensamblados, el trimmer no los detectará y asumirá que no se están usando, lo que provocará errores en tiempo de ejecución.
Para indicar que un ensamblado debe ser incluido obligatoriamente en el paquete de publicación, podemos usar el elemento
<TrimmerRootAssembly>
en el .csproj
:<ItemGroup>
<TrimmerRootAssembly Include="System.Xml.XmlSerializer.dll" />
</ItemGroup>
Publicación single-file: ¡un único ejecutable!
A partir de .NET Core 3, tenemos disponible un nuevo modelo de distribución que permite incluir en un único ejecutable todo lo necesario para que nuestra aplicación funcione sin tener nada preinstalado.Podemos publicar como archivo único mediante una orden de la CLI como la siguiente:
D:\MyConsoleApp>dotnet publish -r win10-x64 -p:PublishSingleFile=true -o output
Microsoft (R) Build Engine versión 16.4.0+e901037fe para .NET Core
Copyright (C) Microsoft Corporation. Todos los derechos reservados.
Restauración realizada en 124,52 ms para D:\MyConsoleApp\MyConsoleApp.csproj.
MyConsoleApp -> D:\MyConsoleApp\bin\Debug\netcoreapp3.1\win10-x64\MyConsoleApp.dll
Se está optimizando el tamaño de los ensamblados, lo que puede cambiar el
comportamiento de la aplicación. Asegúrese de probarlo después de publicar.
Consulte https://aka.ms/dotnet-illink
MyConsoleApp -> D:\MyConsoleApp\output\
D:\MyConsoleApp>dir output
El volumen de la unidad D es Datos
El número de serie del volumen es: 8CBC-81E3
Directorio de D:\MyConsoleApp\output
09/02/2020 19:10 <DIR> .
09/02/2020 19:10 <DIR> ..
09/02/2020 19:10 26.501.267 MyConsoleApp.exe
09/02/2020 19:10 680 MyConsoleApp.pdb
2 archivos 26.501.947 bytes
2 dirs 462.872.199.168 bytes libres
D:\MyConsoleApp>_
En lugar de tener que indicar tantos parámetros cada vez, también podríamos conseguirlo modificando el archivo .csproj
. Para ello, bastaría con establecer a true
el elemento <PublishSingleFile>
, aunque al hacerlo, además, será obligatorio introducir el runtime de destino en el item <RuntimeIdentifier>
. El resultado final podría ser algo así (trimming incluido):<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<PublishSingleFile>true</PublishSingleFile>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
En cualquier caso, fijaos que podríamos distribuir la aplicación, el runtime y el framework en un único ejecutable de 26Mb (usando el trimmer).Publicado en Variable not found.
13 Comentarios:
Hola José María.
Creo que es interesante indicar que en el último caso, el de distribuir la aplicación completa en un único ejecutable, durante la primera ejecución, esta extrae todos los ensamblados y sus dependencias en un directorio temporal, lo que da como resultado que la primera ejecución tarda unos segundos extra en arrancar.
Hola!
Muchas gracias por la aportación :) En efecto, así es; afortunadamente, como indicas, esto solo ocurre la primera vez que la ejecutas, por lo que esos segundos extra son asumibles en muchos casos.
Un saludo!
¿Se puede usar esto en contenedores Docker? Recién hice un hola mundo en Docker con C# y Go. La imagen de Go pesa 6.5 MB y la de C# 105MB y ya usando Alpine.
Hola, Benjamín!
Pues sí, debería funcionar con docker, aunque he de decirte que el "single-file" no lo he probado. Si tienes ocasión de hacerlo, espero que puedas escribirlo en tu blog o al menos comentar por aquí si has encontrado algún inconveniente :)
Saludos!
Excelente artículo, como siempre. Comentar solamente que el "single-file" no es compatible con la publicación en IIS
Muchas gracias por la aportación, desconocía ese detalle :)
Saludos!
No me funciona con la plantilla básica de Visual Studio :-(
Habilito stdoutLogEnabled="true" en el web.config y ninguna pista. Solo recibo un error 500, alguna pista? Gracias de antemano!
Me respondo a mi mismo:
Tras publicar, en el fichero web.config que se genera automáticamente he tenido que cambiar el valor de modules="AspNetCoreModuleV2" a modules="AspNetCoreModule". Me resulta extraño que tenga que hacer esto cada vez que publico... no sé si sería posible establecerlo de alguna manera con anterioridad.
Por cierto,
Una vez más gran artículo!
Un saludo
Hola, Rubén!
¿Probaste a establecer el nombre del módulo en el .csproj mediante el tag AspNetCoreModule?
Saludos!
Sí pero... aparece este error "TransformWebConfig task failed unexpectedly. In process hosting is not supported for AspNetCoreModule. Change the AspNetCoreModule to at least AspNetCoreModuleV2" :-(
Por otra parte,
Al publicar con la opción "single-file" un API en la que utilizo Swagger utilizando el fichero XML para incluir comentarios, no incluye este fichero físicamente y por tanto no funciona. Quizás lo embeba de alguna manera en el ejecutable...
Gracias por tu ayuda!
Hola!
Pues tiene sentido, porque la v2 fue la que introdujo el modo "in-process". En cualquier caso, puede que este tipo de hosting en IIS sea incompatible con el single-file y tenga que ser utilizado como out of process.
Respecto al XML, no lo he probado, pero debería haber alguna forma... ¿probaste a copiarlo, aunque sea a mano, en el directorio del exe? Si funcionase de esta forma, más adelante podrías hacer esa copia a mano a través de tags del csproj.
Complementando lo que comenta Benjamín y José María para el caso de docker una buena manera es con los siguientes comandos en el Dockerfile, usando alpine para tener un contenedor mucho más liviano
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine3.12
...
RUN dotnet restore -r linux-musl-x64
...
RUN dotnet publish -c Release -o /app -r linux-musl-x64
# Y tener cuidado en el entrypoint que debería quedar así:
ENTRYPOINT ["./"]
Esto entendiendo que sea linux, usando alpine vas a tener un contenedor 60% más liviano que el estándar de .net que es basado en Debian.
Un saludo, gracias José María por el artículo.
Un artículo ameno y súper bien explicado. Muchísimas gracias.
Enviar un nuevo comentario